mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? When migrating from v1 to v2 task queue persistence, when the "draining" queue is empty, unload it and mark it in the active queue's metadata so that it won't be reloaded in the future. ## Why? There's no need to reload drained queues anymore. ## How did you test it? - [x] added new unit test(s) - [x] added new functional test(s) ## Potential risks Incorrect identification of empty queues
1016 lines
34 KiB
Go
1016 lines
34 KiB
Go
package tests
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
activitypb "go.temporal.io/api/activity/v1"
|
|
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"
|
|
workflowpb "go.temporal.io/api/workflow/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/common/dynamicconfig"
|
|
"go.temporal.io/server/common/metrics"
|
|
"go.temporal.io/server/common/payloads"
|
|
"go.temporal.io/server/common/serviceerror"
|
|
"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"
|
|
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
|
)
|
|
|
|
type PrioritySuite struct {
|
|
testcore.FunctionalTestBase
|
|
}
|
|
|
|
func TestPrioritySuite(t *testing.T) {
|
|
t.Parallel()
|
|
suite.Run(t, new(PrioritySuite))
|
|
}
|
|
|
|
func (s *PrioritySuite) SetupSuite() {
|
|
dynamicConfigOverrides := map[dynamicconfig.Key]any{
|
|
dynamicconfig.MatchingUseNewMatcher.Key(): true,
|
|
dynamicconfig.MatchingGetTasksBatchSize.Key(): 20,
|
|
dynamicconfig.MatchingGetTasksReloadAt.Key(): 5,
|
|
}
|
|
s.FunctionalTestBase.SetupSuiteWithCluster(testcore.WithDynamicConfigOverrides(dynamicConfigOverrides))
|
|
}
|
|
|
|
func (s *PrioritySuite) TestActivity_Basic() {
|
|
const N = 100
|
|
const Levels = 5
|
|
|
|
tv := testvars.New(s.T())
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
for wfidx := range N {
|
|
_, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowId: fmt.Sprintf("wf%d", wfidx),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
}
|
|
|
|
// process workflow tasks
|
|
for range N {
|
|
_, err := s.TaskPoller().PollAndHandleWorkflowTask(
|
|
tv,
|
|
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
|
|
s.Len(task.History.Events, 3)
|
|
|
|
var wfidx int
|
|
_, err := fmt.Sscanf(task.WorkflowExecution.WorkflowId, "wf%d", &wfidx)
|
|
s.NoError(err)
|
|
|
|
var commands []*commandpb.Command
|
|
|
|
for i, pri := range rand.Perm(Levels) {
|
|
pri += 1 // 1-based
|
|
input, err := payloads.Encode(wfidx, pri)
|
|
s.NoError(err)
|
|
priMsg := &commonpb.Priority{PriorityKey: int32(pri)}
|
|
if pri == (Levels+1)/2 {
|
|
priMsg = nil // nil should be treated as default (3)
|
|
}
|
|
commands = append(commands, &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{
|
|
ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: fmt.Sprintf("act%d", i),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: durationpb.New(time.Minute),
|
|
Priority: priMsg,
|
|
Input: input,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return &workflowservice.RespondWorkflowTaskCompletedRequest{Commands: commands}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.NoError(err)
|
|
}
|
|
|
|
// process activity tasks
|
|
var runs []int
|
|
for range N * Levels {
|
|
_, err := s.TaskPoller().PollAndHandleActivityTask(
|
|
tv,
|
|
func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) {
|
|
var wfidx, pri int
|
|
s.NoError(payloads.Decode(task.Input, &wfidx, &pri))
|
|
// s.T().Log("activity", "pri", pri, "wfidx", wfidx)
|
|
runs = append(runs, pri)
|
|
nothing, err := payloads.Encode()
|
|
s.NoError(err)
|
|
return &workflowservice.RespondActivityTaskCompletedRequest{Result: nothing}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.NoError(err)
|
|
}
|
|
|
|
w := wrongorderness(runs)
|
|
s.T().Log("wrongorderness:", w)
|
|
s.Less(w, 0.1)
|
|
}
|
|
|
|
func (s *PrioritySuite) TestSubqueue_Migration() {
|
|
tv := testvars.New(s.T())
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// start with old matcher
|
|
s.OverrideDynamicConfig(dynamicconfig.MatchingUseNewMatcher, false)
|
|
|
|
// start 100 workflows
|
|
for range 100 {
|
|
_, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowId: uuid.NewString(),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
}
|
|
|
|
// process workflow tasks and create 300 activities
|
|
for range 100 {
|
|
_, err := s.TaskPoller().PollAndHandleWorkflowTask(
|
|
tv,
|
|
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
|
|
s.Len(task.History.Events, 3)
|
|
|
|
var commands []*commandpb.Command
|
|
|
|
for i := range 3 {
|
|
input, err := payloads.Encode(i)
|
|
s.NoError(err)
|
|
commands = append(commands, &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{
|
|
ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: fmt.Sprintf("act%d", i),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: durationpb.New(time.Minute),
|
|
Input: input,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return &workflowservice.RespondWorkflowTaskCompletedRequest{Commands: commands}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.NoError(err)
|
|
}
|
|
|
|
processActivity := func() {
|
|
s.EventuallyWithT(func(c *assert.CollectT) {
|
|
_, err := s.TaskPoller().PollAndHandleActivityTask(
|
|
tv,
|
|
func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) {
|
|
nothing, err := payloads.Encode()
|
|
s.NoError(err)
|
|
return &workflowservice.RespondActivityTaskCompletedRequest{Result: nothing}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
assert.NoError(c, err)
|
|
}, 5*time.Second, time.Millisecond)
|
|
}
|
|
|
|
s.T().Log("process first 100 activities")
|
|
for range 100 {
|
|
processActivity()
|
|
}
|
|
|
|
s.T().Log("switching to new matcher")
|
|
s.OverrideDynamicConfig(dynamicconfig.MatchingUseNewMatcher, true)
|
|
|
|
s.T().Log("processing next 100 activities")
|
|
for range 100 {
|
|
processActivity()
|
|
}
|
|
|
|
s.T().Log("switching back to old matcher")
|
|
s.OverrideDynamicConfig(dynamicconfig.MatchingUseNewMatcher, false)
|
|
|
|
s.T().Log("processing last 100 activities")
|
|
for range 100 {
|
|
processActivity()
|
|
}
|
|
}
|
|
|
|
func (s *PrioritySuite) TestStickyInteraction_SinglePartition() {
|
|
const N = 10
|
|
|
|
tv := testvars.New(s.T())
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// one partition for now:
|
|
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1)
|
|
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1)
|
|
// set these to make the mechanism react faster for the test:
|
|
shortTime := 20 * time.Millisecond
|
|
s.OverrideDynamicConfig(dynamicconfig.MatchingBacklogNegligibleAge, shortTime)
|
|
s.OverrideDynamicConfig(dynamicconfig.MatchingEphemeralDataUpdateInterval, shortTime)
|
|
|
|
describeSticky := func() (*adminservice.DescribeTaskQueuePartitionResponse, error) {
|
|
return s.AdminClient().DescribeTaskQueuePartition(ctx, &adminservice.DescribeTaskQueuePartitionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
TaskQueuePartition: &taskqueuespb.TaskQueuePartition{
|
|
TaskQueue: tv.TaskQueue().Name,
|
|
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
|
|
PartitionId: &taskqueuespb.TaskQueuePartition_StickyName{StickyName: tv.StickyTaskQueue().Name},
|
|
},
|
|
BuildIds: &taskqueuepb.TaskQueueVersionSelection{Unversioned: true},
|
|
})
|
|
}
|
|
|
|
// poll sticky queue once, otherwise it won't be used
|
|
s.T().Log("polling sticky to load")
|
|
stickyPoller := s.TaskPoller().PollWorkflowTask(&workflowservice.PollWorkflowTaskQueueRequest{
|
|
TaskQueue: &taskqueuepb.TaskQueue{
|
|
Name: tv.StickyTaskQueue().Name,
|
|
Kind: enumspb.TASK_QUEUE_KIND_STICKY,
|
|
NormalName: tv.TaskQueue().Name,
|
|
},
|
|
})
|
|
stickyCtx, stickyCancel := context.WithCancel(ctx)
|
|
go stickyPoller.HandleTask(tv, taskpoller.DrainWorkflowTask, taskpoller.WithContext(stickyCtx)) // nolint:errcheck
|
|
// wait for poll to reach matching service and load the queue
|
|
s.EventuallyWithT(func(c *assert.CollectT) {
|
|
res, err := describeSticky()
|
|
require.NoError(c, err)
|
|
require.NotEmpty(c, res.VersionsInfoInternal[""].GetPhysicalTaskQueueInfo().GetPollers())
|
|
}, 5*time.Second, 10*time.Millisecond)
|
|
// cancel as soon as it's registered
|
|
s.T().Log("canceling sticky poll")
|
|
stickyCancel()
|
|
// wait for cancel to propagate
|
|
time.Sleep(100 * time.Millisecond) // nolint:forbidigo // there's no way to wait for this
|
|
|
|
// create some wfts at default priority
|
|
s.T().Log("creating wfts on normal")
|
|
for wfidx := range N {
|
|
_, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowId: fmt.Sprintf("wf%d", wfidx),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
}
|
|
|
|
// process initial tasks on normal queue
|
|
s.T().Log("processing wfts")
|
|
for range N {
|
|
_, err := s.TaskPoller().PollAndHandleWorkflowTask(
|
|
tv,
|
|
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
|
|
return &workflowservice.RespondWorkflowTaskCompletedRequest{
|
|
Commands: []*commandpb.Command{&commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_START_TIMER,
|
|
Attributes: &commandpb.Command_StartTimerCommandAttributes{
|
|
StartTimerCommandAttributes: &commandpb.StartTimerCommandAttributes{
|
|
TimerId: uuid.NewString(),
|
|
StartToFireTimeout: durationpb.New(time.Millisecond),
|
|
},
|
|
},
|
|
}},
|
|
StickyAttributes: &taskqueuepb.StickyExecutionAttributes{
|
|
WorkerTaskQueue: tv.StickyTaskQueue(),
|
|
ScheduleToStartTimeout: durationpb.New(10 * time.Second),
|
|
},
|
|
}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.NoError(err)
|
|
}
|
|
|
|
// after 1 millisecond, we should have N tasks queued on the sticky queue at normal priority
|
|
s.T().Log("checking sticky backlog")
|
|
s.EventuallyWithT(func(c *assert.CollectT) {
|
|
res, err := describeSticky()
|
|
require.NoError(c, err)
|
|
require.EqualValues(c, N, res.VersionsInfoInternal[""].PhysicalTaskQueueInfo.TaskQueueStats.ApproximateBacklogCount)
|
|
}, 5*time.Second, 10*time.Millisecond)
|
|
|
|
// create N more workflows at high priority and N at lower
|
|
s.T().Log("creating high/low wfts on normal")
|
|
for wfidx := range N {
|
|
_, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowId: fmt.Sprintf("highpri%d", wfidx),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
Priority: &commonpb.Priority{PriorityKey: 1},
|
|
})
|
|
s.NoError(err)
|
|
_, err = s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowId: fmt.Sprintf("lowpri%d", wfidx),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
Priority: &commonpb.Priority{PriorityKey: 5},
|
|
})
|
|
s.NoError(err)
|
|
}
|
|
|
|
// the ephemeral data mechanism is asynchronous, so wait a little while for it to kick in.
|
|
// there's no way to check if this is done yet without actually polling, which would mess
|
|
// up the results, so just sleep.
|
|
time.Sleep(3 * shortTime) // nolint:forbidigo
|
|
|
|
// now poll the sticky queue. we should get the high priority tasks from the normal queue
|
|
// then the normal-priority from sticky, then the low priority from normal.
|
|
s.T().Log("polling sticky")
|
|
var receivedOrder []string
|
|
for range 3 * N {
|
|
_, _ = stickyPoller.HandleTask(
|
|
tv,
|
|
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
|
|
wfid := task.WorkflowExecution.WorkflowId
|
|
s.T().Log("task for wf", wfid)
|
|
receivedOrder = append(receivedOrder, wfid)
|
|
return nil, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
}
|
|
|
|
// validate the order
|
|
var priorityOrder []int
|
|
for _, wfid := range receivedOrder {
|
|
if strings.HasPrefix(wfid, "highpri") {
|
|
priorityOrder = append(priorityOrder, 1)
|
|
} else if strings.HasPrefix(wfid, "lowpri") {
|
|
priorityOrder = append(priorityOrder, 3)
|
|
} else {
|
|
priorityOrder = append(priorityOrder, 2) // default priority (wf*)
|
|
}
|
|
}
|
|
|
|
w := wrongorderness(priorityOrder)
|
|
s.T().Log("wrongorderness:", w)
|
|
s.Less(w, 0.1, "tasks should mostly arrive in priority order (high, default, low)")
|
|
}
|
|
|
|
func wrongorderness(vs []int) float64 {
|
|
l := len(vs)
|
|
wrong := 0
|
|
for i, v := range vs[:l-1] {
|
|
for _, w := range vs[i+1:] {
|
|
if v > w {
|
|
wrong++
|
|
}
|
|
}
|
|
}
|
|
return float64(wrong) / float64(l*(l-1)/2)
|
|
}
|
|
|
|
type FairnessSuite struct {
|
|
testcore.FunctionalTestBase
|
|
partitions int
|
|
doAutoEnable bool
|
|
}
|
|
|
|
func TestFairnessSuite(t *testing.T) {
|
|
t.Parallel()
|
|
suite.Run(t, new(FairnessSuite))
|
|
}
|
|
|
|
func TestFairnessAutoEnableSuite(t *testing.T) {
|
|
t.Parallel()
|
|
suite.Run(t, &FairnessSuite{doAutoEnable: true})
|
|
}
|
|
|
|
func (s *FairnessSuite) SetupSuite() {
|
|
s.partitions = 1
|
|
dynamicConfigOverrides := map[dynamicconfig.Key]any{
|
|
dynamicconfig.MatchingGetTasksBatchSize.Key(): 20,
|
|
dynamicconfig.MatchingGetTasksReloadAt.Key(): 5,
|
|
dynamicconfig.NumPendingActivitiesLimitError.Key(): 1000,
|
|
// TODO: disable this and use default later?
|
|
dynamicconfig.MatchingNumTaskqueueReadPartitions.Key(): s.partitions,
|
|
dynamicconfig.MatchingNumTaskqueueWritePartitions.Key(): s.partitions,
|
|
}
|
|
if s.doAutoEnable {
|
|
dynamicConfigOverrides[dynamicconfig.MatchingAutoEnableV2.Key()] = true
|
|
dynamicConfigOverrides[dynamicconfig.MatchingEnableMigration.Key()] = true
|
|
dynamicConfigOverrides[dynamicconfig.MatchingUseNewMatcher.Key()] = false
|
|
dynamicConfigOverrides[dynamicconfig.MatchingEnableFairness.Key()] = false
|
|
} else {
|
|
dynamicConfigOverrides[dynamicconfig.MatchingUseNewMatcher.Key()] = true
|
|
dynamicConfigOverrides[dynamicconfig.MatchingEnableFairness.Key()] = true
|
|
}
|
|
s.FunctionalTestBase.SetupSuiteWithCluster(testcore.WithDynamicConfigOverrides(dynamicConfigOverrides))
|
|
}
|
|
|
|
func (s *FairnessSuite) TriggerAutoEnable(tv *testvars.TestVars) {
|
|
_, err := s.FrontendClient().StartWorkflowExecution(context.Background(), &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowId: "trigger",
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
Priority: &commonpb.Priority{
|
|
PriorityKey: 3,
|
|
},
|
|
})
|
|
s.Require().NoError(err)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
s.Eventually(func() bool {
|
|
resp, err := s.AdminClient().GetTaskQueueTasks(ctx, &adminservice.GetTaskQueueTasksRequest{
|
|
Namespace: s.Namespace().String(),
|
|
TaskQueue: tv.TaskQueue().Name,
|
|
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
|
|
BatchSize: 10,
|
|
MinPass: 1,
|
|
})
|
|
return err == nil && len(resp.GetTasks()) == 1
|
|
}, 10*time.Second, 100*time.Millisecond)
|
|
|
|
_, err = s.TaskPoller().PollAndHandleWorkflowTask(tv,
|
|
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
|
|
var commands []*commandpb.Command
|
|
commands = append(commands,
|
|
&commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{
|
|
ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: "trigger",
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: durationpb.New(time.Minute),
|
|
},
|
|
},
|
|
},
|
|
)
|
|
return &workflowservice.RespondWorkflowTaskCompletedRequest{Commands: commands}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.Require().NoError(err)
|
|
|
|
_, err = s.TaskPoller().PollAndHandleActivityTask(
|
|
tv,
|
|
func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) {
|
|
return &workflowservice.RespondActivityTaskCompletedRequest{}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.Require().NoError(err)
|
|
|
|
_, err = s.FrontendClient().DeleteWorkflowExecution(ctx, &workflowservice.DeleteWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowExecution: &commonpb.WorkflowExecution{
|
|
WorkflowId: "trigger",
|
|
},
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
cancel()
|
|
}
|
|
|
|
func (s *FairnessSuite) Test_Activity_Basic() {
|
|
const Workflows = 15
|
|
const Tasks = 15
|
|
const Keys = 10
|
|
|
|
tv := testvars.New(s.T())
|
|
if s.doAutoEnable {
|
|
s.TriggerAutoEnable(tv)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
zipf := rand.NewZipf(rand.New(rand.NewSource(12345)), 2, 2, Keys-1)
|
|
|
|
for wfidx := range Workflows {
|
|
_, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowId: fmt.Sprintf("wf%d", wfidx),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
}
|
|
|
|
// process workflow tasks
|
|
for range Workflows {
|
|
_, err := s.TaskPoller().PollAndHandleWorkflowTask(
|
|
tv,
|
|
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
|
|
s.Len(task.History.Events, 3)
|
|
|
|
var wfidx int
|
|
_, err := fmt.Sscanf(task.WorkflowExecution.WorkflowId, "wf%d", &wfidx)
|
|
s.NoError(err)
|
|
|
|
var commands []*commandpb.Command
|
|
|
|
for i := range Tasks {
|
|
fkey := int(zipf.Uint64())
|
|
input, err := payloads.Encode(wfidx, fkey)
|
|
s.NoError(err)
|
|
commands = append(commands, &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{
|
|
ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: fmt.Sprintf("act%d", i),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: durationpb.New(time.Minute),
|
|
Priority: &commonpb.Priority{
|
|
FairnessKey: fmt.Sprintf("key%d", fkey),
|
|
},
|
|
Input: input,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return &workflowservice.RespondWorkflowTaskCompletedRequest{Commands: commands}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.NoError(err)
|
|
}
|
|
|
|
// process activity tasks
|
|
var runs []int
|
|
for range Workflows * Tasks {
|
|
_, err := s.TaskPoller().PollAndHandleActivityTask(
|
|
tv,
|
|
func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) {
|
|
var wfidx, fkey int
|
|
s.NoError(payloads.Decode(task.Input, &wfidx, &fkey))
|
|
s.T().Log("activity", "fkey", fkey, "wfidx", wfidx)
|
|
runs = append(runs, fkey)
|
|
nothing, err := payloads.Encode()
|
|
s.NoError(err)
|
|
return &workflowservice.RespondActivityTaskCompletedRequest{Result: nothing}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.NoError(err)
|
|
}
|
|
|
|
u := unfairness(runs)
|
|
s.T().Log("unfairness:", u)
|
|
s.Less(u, 1.0)
|
|
}
|
|
|
|
func unfairness(vs []int) float64 {
|
|
firsts := make(map[int]int)
|
|
for i, v := range vs {
|
|
if _, ok := firsts[v]; !ok {
|
|
firsts[v] = i
|
|
}
|
|
}
|
|
var totalDelay int
|
|
for _, first := range firsts {
|
|
totalDelay += first
|
|
}
|
|
return float64(totalDelay) / float64(len(firsts)*len(firsts))
|
|
}
|
|
|
|
func (s *FairnessSuite) testMigration(newMatcher, fairness bool) {
|
|
tv := testvars.New(s.T())
|
|
|
|
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)
|
|
|
|
forTest := func(v any) any {
|
|
return []dynamicconfig.ConstrainedValue{
|
|
// test tqs (both wf and activity)
|
|
dynamicconfig.ConstrainedValue{
|
|
Constraints: dynamicconfig.Constraints{
|
|
Namespace: s.Namespace().String(),
|
|
TaskQueueName: tv.TaskQueue().Name,
|
|
},
|
|
Value: v,
|
|
},
|
|
// default (match values in SetupSuite to avoid flapping)
|
|
dynamicconfig.ConstrainedValue{Value: true},
|
|
}
|
|
}
|
|
setConfig := func(stage string, newNewMatcher, newFairness bool) {
|
|
newMatcher, fairness = newNewMatcher, newFairness
|
|
s.T().Log("setting config: "+stage, "newMatcher", newMatcher, "fairness", fairness)
|
|
s.OverrideDynamicConfig(dynamicconfig.MatchingUseNewMatcher, forTest(newMatcher))
|
|
s.OverrideDynamicConfig(dynamicconfig.MatchingEnableFairness, forTest(fairness))
|
|
}
|
|
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)
|
|
require.NoError(c, err)
|
|
require.Equal(c, onDraining, tasksOnDraining)
|
|
require.Equal(c, onActive, tasksOnActive)
|
|
}, 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)
|
|
require.NoError(c, err)
|
|
require.False(c, hasDraining, "draining queue should be unloaded after drain completes")
|
|
}, 15*time.Second, 250*time.Millisecond)
|
|
}
|
|
|
|
setConfig("initial", newMatcher, fairness)
|
|
|
|
// start 20 workflows. 20 tasks will be queued on wft queue.
|
|
s.T().Log("starting workflows")
|
|
for range 20 {
|
|
_, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowId: uuid.NewString(),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
})
|
|
s.NoError(err)
|
|
}
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_WORKFLOW, 0, 20)
|
|
|
|
processWft := func() {
|
|
s.EventuallyWithT(func(c *assert.CollectT) {
|
|
_, err := s.TaskPoller().PollAndHandleWorkflowTask(
|
|
tv,
|
|
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
|
|
s.Len(task.History.Events, 3)
|
|
|
|
var commands []*commandpb.Command
|
|
|
|
for i := range 2 {
|
|
input, err := payloads.Encode(i)
|
|
s.NoError(err)
|
|
commands = append(commands, &commandpb.Command{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{
|
|
ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: fmt.Sprintf("act%d", i),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
ScheduleToCloseTimeout: durationpb.New(time.Minute),
|
|
Input: input,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return &workflowservice.RespondWorkflowTaskCompletedRequest{Commands: commands}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
assert.NoError(c, err)
|
|
}, 5*time.Second, time.Millisecond)
|
|
}
|
|
|
|
// process half the workflow tasks and create two activities each.
|
|
// 10 tasks will be left on old workflow queue, 20 tasks will be queued on current activity queue.
|
|
s.T().Log("processing first half of wfts")
|
|
for range 10 {
|
|
processWft()
|
|
}
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_WORKFLOW, 0, 10)
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_ACTIVITY, 0, 20)
|
|
|
|
// switch fairness. queues will be reloaded. wft queue should drain old queue.
|
|
setConfig("switching fairness", true, !fairness)
|
|
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_WORKFLOW, 10, 0)
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_ACTIVITY, 20, 0)
|
|
|
|
// process the other half of workflow tasks. these should come from the draining queue now.
|
|
// 20 tasks will be queued on new activity queue (still 20 on old).
|
|
s.T().Log("processing last half of wfts")
|
|
for range 5 {
|
|
processWft()
|
|
}
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_WORKFLOW, 5, 0)
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_ACTIVITY, 20, 10)
|
|
for range 5 {
|
|
processWft()
|
|
}
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_WORKFLOW, 0, 0)
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_ACTIVITY, 20, 20)
|
|
// Verify wft draining queue is unloaded after drain completes
|
|
waitForNoDraining(enumspb.TASK_QUEUE_TYPE_WORKFLOW)
|
|
s.T().Log("wfts done")
|
|
|
|
// process activities 1/3 at a time
|
|
processActivity := func() {
|
|
s.EventuallyWithT(func(c *assert.CollectT) {
|
|
_, err := s.TaskPoller().PollAndHandleActivityTask(
|
|
tv,
|
|
func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) {
|
|
nothing, err := payloads.Encode()
|
|
s.NoError(err)
|
|
return &workflowservice.RespondActivityTaskCompletedRequest{Result: nothing}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
assert.NoError(c, err)
|
|
}, 5*time.Second, time.Millisecond)
|
|
}
|
|
|
|
s.T().Log("processing first 1/3 activities")
|
|
for range 13 {
|
|
processActivity()
|
|
}
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_ACTIVITY, 7, 20)
|
|
|
|
setConfig("switching fairness again", true, !fairness)
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_ACTIVITY, 20, 7)
|
|
|
|
s.T().Log("processing next 1/3 activities")
|
|
for range 14 {
|
|
processActivity()
|
|
}
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_ACTIVITY, 6, 7)
|
|
|
|
setConfig("switching fairness last time", true, !fairness)
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_ACTIVITY, 7, 6)
|
|
|
|
s.T().Log("processing last 1/3 activities")
|
|
for range 13 {
|
|
processActivity()
|
|
}
|
|
waitForTasks(enumspb.TASK_QUEUE_TYPE_ACTIVITY, 0, 0)
|
|
// Verify activity draining queue is unloaded after drain completes
|
|
waitForNoDraining(enumspb.TASK_QUEUE_TYPE_ACTIVITY)
|
|
}
|
|
|
|
func (s *FairnessSuite) countTasksByDrainingActive(ctx context.Context, tv *testvars.TestVars, tp enumspb.TaskQueueType) (
|
|
tasksOnDraining, tasksOnActive int64, hasDraining bool, retErr error,
|
|
) {
|
|
for i := range s.partitions {
|
|
res, err := s.AdminClient().DescribeTaskQueuePartition(ctx, &adminservice.DescribeTaskQueuePartitionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
TaskQueuePartition: &taskqueuespb.TaskQueuePartition{
|
|
TaskQueue: tv.TaskQueue().Name,
|
|
TaskQueueType: tp,
|
|
PartitionId: &taskqueuespb.TaskQueuePartition_NormalPartitionId{NormalPartitionId: int32(i)},
|
|
},
|
|
BuildIds: &taskqueuepb.TaskQueueVersionSelection{Unversioned: true},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, false, err
|
|
}
|
|
for _, versionInfoInternal := range res.VersionsInfoInternal {
|
|
for _, st := range versionInfoInternal.PhysicalTaskQueueInfo.InternalTaskQueueStatus {
|
|
if st.Draining {
|
|
hasDraining = true
|
|
tasksOnDraining += st.ApproximateBacklogCount
|
|
} else {
|
|
tasksOnActive += st.ApproximateBacklogCount
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *FairnessSuite) TestMigration_FromClassic() {
|
|
// classic->fair, fair->pri. fair metadata will be created on transition.
|
|
s.testMigration(false, false)
|
|
}
|
|
|
|
func (s *FairnessSuite) TestMigration_FromPri() {
|
|
// pri->fair, fair->pri. fair metadata will be created before transition.
|
|
s.testMigration(true, false)
|
|
}
|
|
|
|
func (s *FairnessSuite) TestMigration_FromFair() {
|
|
// fair->pri, pri->fair. fair metadata will be created first.
|
|
s.testMigration(true, true)
|
|
}
|
|
|
|
func (s *FairnessSuite) TestUpdateWorkflowExecutionOptions_InvalidatesPendingTask() {
|
|
if s.doAutoEnable {
|
|
s.T().Skip("flaky with autoenable")
|
|
}
|
|
tv := testvars.New(s.T())
|
|
capture := s.GetTestCluster().Host().CaptureMetricsHandler().StartCapture()
|
|
defer s.GetTestCluster().Host().CaptureMetricsHandler().StopCapture(capture)
|
|
|
|
ctx, cancel := context.WithTimeout(s.T().Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
originalPriority := &commonpb.Priority{FairnessKey: "KEY"}
|
|
updatedPriority := &commonpb.Priority{FairnessKey: "NEW_KEY"}
|
|
|
|
// Queue up new workflow.
|
|
startResp, err := s.FrontendClient().StartWorkflowExecution(ctx, &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowId: tv.WorkflowID(),
|
|
WorkflowType: tv.WorkflowType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
Priority: originalPriority,
|
|
})
|
|
s.NoError(err)
|
|
|
|
// Wait for workflow task to be backlogged.
|
|
s.Eventually(func() bool {
|
|
resp, err := s.AdminClient().GetTaskQueueTasks(ctx, &adminservice.GetTaskQueueTasksRequest{
|
|
Namespace: s.Namespace().String(),
|
|
TaskQueue: tv.TaskQueue().Name,
|
|
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
|
|
BatchSize: 10,
|
|
MinPass: 1,
|
|
})
|
|
return err == nil && len(resp.GetTasks()) == 1
|
|
}, 10*time.Second, 100*time.Millisecond)
|
|
|
|
// Update workflow options to set a new priority.
|
|
updateResp, err := s.FrontendClient().UpdateWorkflowExecutionOptions(ctx, &workflowservice.UpdateWorkflowExecutionOptionsRequest{
|
|
Namespace: s.Namespace().String(),
|
|
WorkflowExecution: &commonpb.WorkflowExecution{
|
|
WorkflowId: tv.WorkflowID(),
|
|
RunId: startResp.GetRunId(),
|
|
},
|
|
WorkflowExecutionOptions: &workflowpb.WorkflowExecutionOptions{
|
|
Priority: updatedPriority,
|
|
},
|
|
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"priority"}},
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(updateResp.GetWorkflowExecutionOptions())
|
|
s.ProtoEqual(updatedPriority, updateResp.GetWorkflowExecutionOptions().GetPriority())
|
|
|
|
// Query workflow to verify workflow has the updated priority.
|
|
descResp, err := s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
Execution: &commonpb.WorkflowExecution{
|
|
WorkflowId: tv.WorkflowID(),
|
|
RunId: startResp.GetRunId(),
|
|
},
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(descResp.GetWorkflowExecutionInfo())
|
|
s.NotNil(descResp.GetWorkflowExecutionInfo().GetPriority())
|
|
s.ProtoEqual(updatedPriority, descResp.GetWorkflowExecutionInfo().GetPriority())
|
|
|
|
// Poll for workflow task and schedule an activity.
|
|
_, err = s.TaskPoller().PollAndHandleWorkflowTask(
|
|
tv,
|
|
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
|
|
s.NotNil(task)
|
|
s.ContainsHistoryEvents(`
|
|
3 WorkflowExecutionOptionsUpdated { "Priority": { "FairnessKey": "NEW_KEY" } }
|
|
4 WorkflowTaskStarted
|
|
`, task.History.Events)
|
|
|
|
return &workflowservice.RespondWorkflowTaskCompletedRequest{
|
|
Commands: []*commandpb.Command{
|
|
{
|
|
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
|
|
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{
|
|
ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
|
|
ActivityId: tv.ActivityID(),
|
|
ActivityType: tv.ActivityType(),
|
|
TaskQueue: tv.TaskQueue(),
|
|
Priority: originalPriority,
|
|
ScheduleToCloseTimeout: durationpb.New(time.Minute),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.NoError(err)
|
|
|
|
// Verify that 2 workflow tasks were sent to matching; and 1 was marked obsolete.
|
|
addWorkflowTaskCount := 0
|
|
obsoleteWorkflowTaskCount := 0
|
|
snap := capture.Snapshot()
|
|
for _, rec := range snap[metrics.ClientRequests.Name()] {
|
|
for key, val := range rec.Tags {
|
|
if key == metrics.OperationTagName && val == metrics.MatchingClientAddWorkflowTaskScope {
|
|
addWorkflowTaskCount++
|
|
}
|
|
}
|
|
}
|
|
s.Equal(2, addWorkflowTaskCount, "Expected 2 workflow tasks to be dispatched to matching")
|
|
for _, rec := range snap[metrics.ClientFailures.Name()] {
|
|
for key, val := range rec.Tags {
|
|
if key == metrics.ErrorTypeTagName && val == fmt.Sprintf("%T", serviceerror.ObsoleteMatchingTask{}) {
|
|
obsoleteWorkflowTaskCount++
|
|
}
|
|
}
|
|
}
|
|
s.Equal(1, obsoleteWorkflowTaskCount, "Expected 1 workflow task to be obsolete")
|
|
|
|
// Wait for activity task to be backlogged
|
|
s.Eventually(func() bool {
|
|
resp, err := s.AdminClient().GetTaskQueueTasks(ctx, &adminservice.GetTaskQueueTasksRequest{
|
|
Namespace: s.Namespace().String(),
|
|
TaskQueue: tv.TaskQueue().Name,
|
|
TaskQueueType: enumspb.TASK_QUEUE_TYPE_ACTIVITY,
|
|
BatchSize: 10,
|
|
MinPass: 1,
|
|
})
|
|
return err == nil && len(resp.GetTasks()) == 1
|
|
}, 10*time.Second, 100*time.Millisecond)
|
|
|
|
// Update activity options to set a new priority
|
|
_, err = s.FrontendClient().UpdateActivityOptions(ctx, &workflowservice.UpdateActivityOptionsRequest{
|
|
Namespace: s.Namespace().String(),
|
|
Execution: &commonpb.WorkflowExecution{
|
|
WorkflowId: tv.WorkflowID(),
|
|
RunId: startResp.GetRunId(),
|
|
},
|
|
Activity: &workflowservice.UpdateActivityOptionsRequest_Id{Id: tv.ActivityID()},
|
|
ActivityOptions: &activitypb.ActivityOptions{
|
|
Priority: updatedPriority,
|
|
},
|
|
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"priority"}},
|
|
})
|
|
s.NoError(err)
|
|
|
|
// Query workflow to verify activity has the updated priority.
|
|
descResp, err = s.FrontendClient().DescribeWorkflowExecution(ctx, &workflowservice.DescribeWorkflowExecutionRequest{
|
|
Namespace: s.Namespace().String(),
|
|
Execution: &commonpb.WorkflowExecution{
|
|
WorkflowId: tv.WorkflowID(),
|
|
RunId: startResp.GetRunId(),
|
|
},
|
|
})
|
|
s.NoError(err)
|
|
s.NotNil(descResp.GetPendingActivities())
|
|
s.Len(descResp.GetPendingActivities(), 1)
|
|
s.ProtoEqual(updatedPriority, descResp.GetPendingActivities()[0].GetPriority())
|
|
s.ProtoEqual(updatedPriority, descResp.GetPendingActivities()[0].GetActivityOptions().GetPriority())
|
|
|
|
// Poll for activity task and verify it has the updated priority.
|
|
_, err = s.TaskPoller().PollAndHandleActivityTask(
|
|
tv,
|
|
func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) {
|
|
s.NotNil(task)
|
|
s.Equal(tv.ActivityID(), task.ActivityId)
|
|
s.ProtoEqual(updatedPriority, task.Priority)
|
|
return &workflowservice.RespondActivityTaskCompletedRequest{}, nil
|
|
},
|
|
taskpoller.WithContext(ctx),
|
|
)
|
|
s.NoError(err)
|
|
|
|
// Verify that 2 activity tasks were sent to matching; and 1 was marked obsolete
|
|
addActivityTaskCount := 0
|
|
obsoleteActivityTaskCount := 0
|
|
snap = capture.Snapshot()
|
|
for _, rec := range snap[metrics.ClientRequests.Name()] {
|
|
for key, val := range rec.Tags {
|
|
if key == metrics.OperationTagName && val == metrics.MatchingClientAddActivityTaskScope {
|
|
addActivityTaskCount++
|
|
}
|
|
}
|
|
}
|
|
s.Equal(2, addActivityTaskCount, "Expected 2 activity tasks to be dispatched to matching")
|
|
for _, rec := range snap[metrics.ClientFailures.Name()] {
|
|
for key, val := range rec.Tags {
|
|
if key == metrics.ErrorTypeTagName && val == fmt.Sprintf("%T", serviceerror.ObsoleteMatchingTask{}) {
|
|
obsoleteActivityTaskCount++
|
|
}
|
|
}
|
|
}
|
|
s.Equal(1+obsoleteWorkflowTaskCount, obsoleteActivityTaskCount, "Expected 1 activity task to be obsolete")
|
|
}
|