Files
tasks/service/matching/matching_engine_test.go
60c9c75988 jitter poller timeout (#8927)
## What changed?
Add a jitter to our buffer for poller context timeouts

## Why?
we were running in to issues when we had larger numbers of pollers all
timing out together, this should spread out that load a bit more.

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

---------

Co-authored-by: Jacob Moody <[email protected]>
2026-05-22 15:35:10 -05:00

6190 lines
233 KiB
Go

package matching
import (
"context"
"errors"
"fmt"
"math"
"math/rand"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"testing/synctest"
"time"
"github.com/emirpasic/gods/maps/treemap"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/uber-go/tally/v4"
commandpb "go.temporal.io/api/command/v1"
commonpb "go.temporal.io/api/common/v1"
deploymentpb "go.temporal.io/api/deployment/v1"
enumspb "go.temporal.io/api/enums/v1"
historypb "go.temporal.io/api/history/v1"
nexuspb "go.temporal.io/api/nexus/v1"
querypb "go.temporal.io/api/query/v1"
"go.temporal.io/api/serviceerror"
taskqueuepb "go.temporal.io/api/taskqueue/v1"
"go.temporal.io/api/workflowservice/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"
"go.temporal.io/server/api/historyservice/v1"
"go.temporal.io/server/api/historyservicemock/v1"
"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"
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"
"go.temporal.io/server/common/cluster/clustertest"
"go.temporal.io/server/common/collection"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/membership"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/metrics/metricstest"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/common/payload"
"go.temporal.io/server/common/payloads"
"go.temporal.io/server/common/persistence"
"go.temporal.io/server/common/persistence/visibility/manager"
"go.temporal.io/server/common/primitives"
"go.temporal.io/server/common/primitives/timestamp"
"go.temporal.io/server/common/quotas"
"go.temporal.io/server/common/searchattribute"
serviceerrors "go.temporal.io/server/common/serviceerror"
"go.temporal.io/server/common/tasktoken"
"go.temporal.io/server/common/testing/protoassert"
"go.temporal.io/server/common/testing/testlogger"
"go.temporal.io/server/common/testing/testvars"
"go.temporal.io/server/common/tqid"
"go.temporal.io/server/common/worker_versioning"
"go.temporal.io/server/components/nexusoperations"
"go.temporal.io/server/service/history/consts"
"go.uber.org/mock/gomock"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// static error used in tests
var randomTestError = errors.New("random error")
type (
matchingEngineSuite struct {
suite.Suite
*require.Assertions
newMatcher bool
fairness bool
controller *gomock.Controller
mockHistoryClient *historyservicemock.MockHistoryServiceClient
mockMatchingClient *matchingservicemock.MockMatchingServiceClient
ns *namespace.Namespace
mockNamespaceCache *namespace.MockRegistry
mockVisibilityManager *manager.MockVisibilityManager
mockHostInfoProvider *membership.MockHostInfoProvider
mockServiceResolver *membership.MockServiceResolver
hostInfoForResolver membership.HostInfo
mockNexusEndpointManager *persistence.MockNexusEndpointManager
matchingEngine *matchingEngineImpl
taskManager *testTaskManager // points to classicTaskManager or fairTaskManager
classicTaskManager *testTaskManager
fairTaskManager *testTaskManager
logger *testlogger.TestLogger
}
)
const (
matchingTestNamespace = "matching-test"
)
func createTestMatchingEngine(
logger log.Logger,
controller *gomock.Controller,
config *Config,
matchingClient matchingservice.MatchingServiceClient,
namespaceRegistry namespace.Registry,
) *matchingEngineImpl {
tm := newTestTaskManager(logger)
ftm := newTestFairTaskManager(logger)
mockVisibilityManager := manager.NewMockVisibilityManager(controller)
mockVisibilityManager.EXPECT().Close().AnyTimes()
mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(controller)
mockHistoryClient.EXPECT().IsWorkflowTaskValid(gomock.Any(), gomock.Any()).Return(&historyservice.IsWorkflowTaskValidResponse{IsValid: true}, nil).AnyTimes()
mockHistoryClient.EXPECT().IsActivityTaskValid(gomock.Any(), gomock.Any()).Return(&historyservice.IsActivityTaskValidResponse{IsValid: true}, nil).AnyTimes()
mockHostInfoProvider := membership.NewMockHostInfoProvider(controller)
hostInfo := membership.NewHostInfoFromAddress("self")
mockHostInfoProvider.EXPECT().HostInfo().Return(hostInfo).AnyTimes()
mockServiceResolver := membership.NewMockServiceResolver(controller)
mockServiceResolver.EXPECT().Lookup(gomock.Any()).Return(hostInfo, nil).AnyTimes()
mockServiceResolver.EXPECT().AddListener(gomock.Any(), gomock.Any()).AnyTimes()
mockServiceResolver.EXPECT().RemoveListener(gomock.Any()).AnyTimes()
mockNexusEndpointManager := persistence.NewMockNexusEndpointManager(controller)
mockNexusEndpointManager.EXPECT().ListNexusEndpoints(gomock.Any(), gomock.Any()).Return(&persistence.ListNexusEndpointsResponse{}, nil).AnyTimes()
return newMatchingEngine(config, tm, ftm, mockHistoryClient, logger, namespaceRegistry, matchingClient, mockVisibilityManager, mockHostInfoProvider, mockServiceResolver, mockNexusEndpointManager)
}
func createMockNamespaceCache(controller *gomock.Controller, nsName namespace.Name) (*namespace.Namespace, *namespace.MockRegistry) {
ns := namespace.NewLocalNamespaceForTest(&persistencespb.NamespaceInfo{Name: nsName.String(), Id: uuid.NewString()}, nil, "")
mockNamespaceCache := namespace.NewMockRegistry(controller)
mockNamespaceCache.EXPECT().GetNamespaceByID(gomock.Any()).Return(ns, nil).AnyTimes()
mockNamespaceCache.EXPECT().GetNamespaceName(gomock.Any()).Return(ns.Name(), nil).AnyTimes()
return ns, mockNamespaceCache
}
// TODO(pri): cleanup; delete this
func TestMatchingEngine_Classic_Suite(t *testing.T) {
suite.Run(t, &matchingEngineSuite{newMatcher: false})
}
func TestMatchingEngine_Pri_Suite(t *testing.T) {
suite.Run(t, &matchingEngineSuite{newMatcher: true})
}
func TestMatchingEngine_Fair_Suite(t *testing.T) {
suite.Run(t, &matchingEngineSuite{newMatcher: true, fairness: true})
}
func (s *matchingEngineSuite) SetupSuite() {
}
func (s *matchingEngineSuite) TearDownSuite() {
}
func (s *matchingEngineSuite) SetupTest() {
s.Assertions = require.New(s.T())
s.logger = testlogger.NewTestLogger(s.T(), testlogger.FailOnAnyUnexpectedError)
s.controller = gomock.NewController(s.T())
s.mockHistoryClient = historyservicemock.NewMockHistoryServiceClient(s.controller)
s.mockMatchingClient = matchingservicemock.NewMockMatchingServiceClient(s.controller)
s.mockMatchingClient.EXPECT().GetTaskQueueUserData(gomock.Any(), gomock.Any()).
Return(&matchingservice.GetTaskQueueUserDataResponse{}, nil).AnyTimes()
s.mockMatchingClient.EXPECT().UpdateTaskQueueUserData(gomock.Any(), gomock.Any()).
Return(&matchingservice.UpdateTaskQueueUserDataResponse{}, nil).AnyTimes()
s.mockMatchingClient.EXPECT().ReplicateTaskQueueUserData(gomock.Any(), gomock.Any()).
Return(&matchingservice.ReplicateTaskQueueUserDataResponse{}, nil).AnyTimes()
s.mockMatchingClient.EXPECT().ForceLoadTaskQueuePartition(gomock.Any(), gomock.Any()).
Return(&matchingservice.ForceLoadTaskQueuePartitionResponse{WasUnloaded: true}, nil).AnyTimes()
// create and supply two task managers, but only one is expected to be used at a time since
// we run tests with fairness enabled in separate suite.
s.classicTaskManager = newTestTaskManager(s.logger)
s.fairTaskManager = newTestFairTaskManager(s.logger)
if s.fairness {
s.taskManager = s.fairTaskManager
} else {
s.taskManager = s.classicTaskManager
}
s.ns, s.mockNamespaceCache = createMockNamespaceCache(s.controller, matchingTestNamespace)
s.mockVisibilityManager = manager.NewMockVisibilityManager(s.controller)
s.mockVisibilityManager.EXPECT().Close().AnyTimes()
s.mockHostInfoProvider = membership.NewMockHostInfoProvider(s.controller)
hostInfo := membership.NewHostInfoFromAddress("self")
s.hostInfoForResolver = hostInfo
s.mockHostInfoProvider.EXPECT().HostInfo().Return(hostInfo).AnyTimes()
s.mockServiceResolver = membership.NewMockServiceResolver(s.controller)
s.mockServiceResolver.EXPECT().Lookup(gomock.Any()).DoAndReturn(func(string) (membership.HostInfo, error) {
return s.hostInfoForResolver, nil
}).AnyTimes()
s.mockServiceResolver.EXPECT().AddListener(gomock.Any(), gomock.Any()).AnyTimes()
s.mockServiceResolver.EXPECT().RemoveListener(gomock.Any()).AnyTimes()
s.mockNexusEndpointManager = persistence.NewMockNexusEndpointManager(s.controller)
s.mockNexusEndpointManager.EXPECT().ListNexusEndpoints(gomock.Any(), gomock.Any()).Return(&persistence.ListNexusEndpointsResponse{}, nil).AnyTimes()
s.matchingEngine = s.newMatchingEngine(s.newConfig(), s.classicTaskManager, s.fairTaskManager)
s.matchingEngine.Start()
}
func (s *matchingEngineSuite) newConfig() *Config {
res := defaultTestConfig()
if s.fairness {
useFairness(res)
} else if !s.newMatcher {
useClassicMatcher(res)
}
return res
}
func (s *matchingEngineSuite) TearDownTest() {
s.matchingEngine.Stop()
}
func (s *matchingEngineSuite) newMatchingEngine(
config *Config,
taskMgr persistence.TaskManager,
fairTaskMgr persistence.TaskManager,
) *matchingEngineImpl {
return newMatchingEngine(config, taskMgr, fairTaskMgr, s.mockHistoryClient, s.logger, s.mockNamespaceCache, s.mockMatchingClient, s.mockVisibilityManager,
s.mockHostInfoProvider, s.mockServiceResolver, s.mockNexusEndpointManager)
}
func newMatchingEngine(
config *Config, taskMgr persistence.TaskManager, fairTaskMgr persistence.FairTaskManager,
mockHistoryClient historyservice.HistoryServiceClient,
logger log.Logger, mockNamespaceCache namespace.Registry, mockMatchingClient matchingservice.MatchingServiceClient,
mockVisibilityManager manager.VisibilityManager, mockHostInfoProvider membership.HostInfoProvider,
mockServiceResolver membership.ServiceResolver, nexusEndpointManager persistence.NexusEndpointManager,
) *matchingEngineImpl {
e := &matchingEngineImpl{
taskManager: taskMgr,
fairTaskManager: fairTaskMgr,
historyClient: mockHistoryClient,
partitions: make(map[tqid.PartitionKey]taskQueuePartitionManager),
gaugeMetrics: gaugeMetrics{
loadedTaskQueueFamilyCount: make(map[taskQueueCounterKey]int),
loadedTaskQueueCount: make(map[taskQueueCounterKey]int),
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),
}
e.nexusEndpointsOwnershipLostCh.Store(make(chan struct{}))
return e
}
func (s *matchingEngineSuite) newPartitionManager(prtn tqid.Partition, config *Config) taskQueuePartitionManager {
tqConfig := newTaskQueueConfig(prtn.TaskQueue(), config, matchingTestNamespace)
logger, _, metricsHandler := s.matchingEngine.loggerAndMetricsForPartition(s.ns, prtn, tqConfig)
pm, err := newTaskQueuePartitionManager(s.matchingEngine, s.ns, prtn, tqConfig, logger, logger, metricsHandler, &mockUserDataManager{})
s.Require().NoError(err)
return pm
}
// captureNPollDeadlines injects a mock partition manager, runs n sequential polls, and returns
// the context deadline observed inside each pm.PollTask call.
func (s *matchingEngineSuite) captureNPollDeadlines(
partition tqid.Partition,
longPollInterval time.Duration,
meta *pollMetadata,
n int,
) []time.Time {
mockPM := NewMocktaskQueuePartitionManager(s.controller)
mockPM.EXPECT().WaitUntilInitialized(gomock.Any()).Return(nil).Times(n)
mockPM.EXPECT().LongPollExpirationInterval().Return(longPollInterval).Times(n)
mockPM.EXPECT().Stop(gomock.Any()).AnyTimes()
deadlines := make([]time.Time, 0, n)
mockPM.EXPECT().PollTask(gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, _ *pollMetadata) (*internalTask, bool, error) {
dl, _ := ctx.Deadline()
deadlines = append(deadlines, dl)
return nil, false, errNoTasks
},
).Times(n)
s.matchingEngine.partitionsLock.Lock()
s.matchingEngine.partitions[partition.Key()] = mockPM
s.matchingEngine.partitionsLock.Unlock()
for range n {
_, _, _ = s.matchingEngine.pollTask(context.Background(), partition, meta)
}
return deadlines
}
func deadlineSpread(deadlines []time.Time) time.Duration {
lo, hi := deadlines[0], deadlines[0]
for _, d := range deadlines[1:] {
if d.Before(lo) {
lo = d
}
if d.After(hi) {
hi = d
}
}
return hi.Sub(lo)
}
// TestNonForwardedPollsHaveSpreadExpirations verifies that non-forwarded polls receive different
// expiration times, which is the mechanism that prevents a thundering herd of simultaneous worker
// reconnects when a wave of long polls all expire at the same instant.
func (s *matchingEngineSuite) TestNonForwardedPollsHaveSpreadExpirations() {
const n = 20
partition := newRootPartition(s.ns.ID().String(), "spread-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW)
deadlines := s.captureNPollDeadlines(partition, 60*time.Second, &pollMetadata{}, n)
spread := deadlineSpread(deadlines)
s.Greater(spread, 5*time.Second, "non-forwarded polls must have varied expirations to prevent thundering herd")
s.LessOrEqual(spread, time.Duration(float64(60*time.Second)*forwardedPollJitterRatio))
}
// TestForwardedPollsHaveConsistentExpirations verifies that forwarded polls all receive the same
// expiration — jitter was already applied at the leaf partition and must not compound.
func (s *matchingEngineSuite) TestForwardedPollsHaveConsistentExpirations() {
const n = 20
partition := newRootPartition(s.ns.ID().String(), "forwarded-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW)
deadlines := s.captureNPollDeadlines(partition, 60*time.Second, &pollMetadata{forwardedFrom: "leaf"}, n)
s.Less(deadlineSpread(deadlines), 100*time.Millisecond, "forwarded polls must not receive additional jitter")
}
// TestShortIntervalPollsHaveConsistentExpirations verifies that polls with a configured interval
// below the minimum threshold are not jittered — a 10s jitter would be disproportionate for a
// short timeout and is simply skipped.
func (s *matchingEngineSuite) TestShortIntervalPollsHaveConsistentExpirations() {
const n = 20
partition := newRootPartition(s.ns.ID().String(), "short-interval-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW)
deadlines := s.captureNPollDeadlines(partition, forwardedPollMinInterval-time.Second, &pollMetadata{}, n)
s.Less(deadlineSpread(deadlines), 100*time.Millisecond, "polls below the minimum interval must not be jittered")
}
func (s *matchingEngineSuite) TestPollActivityTaskQueuesEmptyResult() {
s.PollForTasksEmptyResultTest(context.Background(), enumspb.TASK_QUEUE_TYPE_ACTIVITY)
}
func (s *matchingEngineSuite) TestPollWorkflowTaskQueuesEmptyResult() {
s.PollForTasksEmptyResultTest(context.Background(), enumspb.TASK_QUEUE_TYPE_WORKFLOW)
}
func (s *matchingEngineSuite) TestPollActivityTaskQueuesEmptyResultWithShortContext() {
shortContextTimeout := returnEmptyTaskTimeBudget + 10*time.Millisecond
callContext, cancel := context.WithTimeout(context.Background(), shortContextTimeout)
defer cancel()
s.PollForTasksEmptyResultTest(callContext, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
}
func (s *matchingEngineSuite) TestPollWorkflowTaskQueuesEmptyResultWithShortContext() {
shortContextTimeout := returnEmptyTaskTimeBudget + 10*time.Millisecond
callContext, cancel := context.WithTimeout(context.Background(), shortContextTimeout)
defer cancel()
s.PollForTasksEmptyResultTest(callContext, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
}
func (s *matchingEngineSuite) PollForTasksEmptyResultTest(callContext context.Context, taskType enumspb.TaskQueueType) {
s.matchingEngine.config.RangeSize = 2 // to test that range is not updated without tasks
if _, ok := callContext.Deadline(); !ok {
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(10 * time.Millisecond)
}
namespaceID := uuid.NewString()
tl := "makeToast"
identity := "selfDrivingToaster"
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
var taskQueueType enumspb.TaskQueueType
tlID := newUnversionedRootQueueKey(namespaceID, tl, taskType)
const pollCount = 10
for range pollCount {
if taskType == enumspb.TASK_QUEUE_TYPE_ACTIVITY {
pollResp, err := s.matchingEngine.PollActivityTaskQueue(callContext, &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
s.NoError(err)
s.Equal(emptyPollActivityTaskQueueResponse, pollResp)
taskQueueType = enumspb.TASK_QUEUE_TYPE_ACTIVITY
} else {
resp, err := s.matchingEngine.PollWorkflowTaskQueue(callContext, &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
s.NoError(err)
s.Equal(emptyPollWorkflowTaskQueueResponse, resp)
taskQueueType = enumspb.TASK_QUEUE_TYPE_WORKFLOW
}
select {
case <-callContext.Done():
s.FailNow("Call context has expired.")
default:
}
// check the poller information
descResp, err := s.matchingEngine.DescribeTaskQueue(context.Background(), &matchingservice.DescribeTaskQueueRequest{
NamespaceId: namespaceID,
DescRequest: &workflowservice.DescribeTaskQueueRequest{
TaskQueue: taskQueue,
TaskQueueType: taskQueueType,
IncludeTaskQueueStatus: false,
},
})
s.NoError(err)
s.Equal(1, len(descResp.DescResponse.Pollers))
s.Equal(identity, descResp.DescResponse.Pollers[0].GetIdentity())
s.NotEmpty(descResp.DescResponse.Pollers[0].GetLastAccessTime())
s.Nil(descResp.DescResponse.GetTaskQueueStatus())
}
s.EqualValues(1, s.taskManager.getQueueDataByKey(tlID).RangeID())
}
func (s *matchingEngineSuite) TestOnlyUnloadMatchingInstance() {
prtn := newRootPartition(
uuid.NewString(),
"makeToast",
enumspb.TASK_QUEUE_TYPE_ACTIVITY)
tqm, _, err := s.matchingEngine.getTaskQueuePartitionManager(context.Background(), prtn, true, loadCauseUnspecified)
s.Require().NoError(err)
tqm2 := s.newPartitionManager(prtn, s.matchingEngine.config)
// try to unload a different tqMgr instance with the same taskqueue ID
s.matchingEngine.unloadTaskQueuePartition(tqm2, unloadCauseUnspecified)
got, _, err := s.matchingEngine.getTaskQueuePartitionManager(context.Background(), prtn, true, loadCauseUnspecified)
s.Require().NoError(err)
s.Require().Same(tqm, got,
"Unload call with non-matching taskQueuePartitionManager should not cause unload")
// this time unload the right tqMgr
s.matchingEngine.unloadTaskQueuePartition(tqm, unloadCauseUnspecified)
got, _, err = s.matchingEngine.getTaskQueuePartitionManager(context.Background(), prtn, true, loadCauseUnspecified)
s.Require().NoError(err)
s.Require().NotSame(tqm, got,
"Unload call with matching incarnation should have caused unload")
}
func (s *matchingEngineSuite) TestFailAddTaskWithHistoryExhausted() {
tqName := "testFailAddTaskWithHistoryExhausted"
historyError := consts.ErrResourceExhaustedBusyWorkflow
s.testFailAddTaskWithHistoryError(tqName, false, historyError, nil)
}
func (s *matchingEngineSuite) TestFailAddTaskWithHistoryError() {
s.logger.Expect(testlogger.Error, "dropping task due to non-nonretryable errors")
historyError := serviceerror.NewInternal("nothing to start")
tqName := "testFailAddTaskWithHistoryError"
s.testFailAddTaskWithHistoryError(tqName, true, historyError, nil) // expectedError shall be nil since history drops the task
}
func (s *matchingEngineSuite) testFailAddTaskWithHistoryError(
tqName string,
expectSyncMatch bool,
recordError error,
expectedError error,
) {
namespaceID := namespace.ID(uuid.NewString())
identity := "identity"
stickyTaskQueue := &taskqueuepb.TaskQueue{Name: tqName, Kind: enumspb.TASK_QUEUE_KIND_STICKY}
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(1 * time.Second)
runID := uuid.NewString()
workflowID := "workflow1"
execution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
scheduledEventID := int64(0)
addRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID.String(),
Execution: execution,
ScheduledEventId: scheduledEventID,
TaskQueue: stickyTaskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
pollRequest := matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID.String(),
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: stickyTaskQueue,
Identity: identity,
},
}
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
_, err := s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &pollRequest, metrics.NoopMetricsHandler)
if err != nil {
s.logger.Info(err.Error())
}
wg.Done()
}()
partitionReady := func() bool {
return len(s.matchingEngine.getTaskQueuePartitions(10)) >= 1
}
s.Eventually(partitionReady, 100*time.Millisecond, 10*time.Millisecond)
recordWorkflowTaskStartedResponse := &historyservice.RecordWorkflowTaskStartedResponse{
PreviousStartedEventId: scheduledEventID,
ScheduledEventId: scheduledEventID + 1,
Attempt: 1,
StickyExecutionEnabled: true,
WorkflowExecutionTaskQueue: &taskqueuepb.TaskQueue{Name: tqName, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
History: &historypb.History{Events: []*historypb.HistoryEvent{}},
NextPageToken: nil,
}
s.mockHistoryClient.EXPECT().
RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, _ *historyservice.RecordWorkflowTaskStartedRequest, _ ...any) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(10 * time.Millisecond)
return nil, recordError
})
s.mockHistoryClient.EXPECT().
RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).
Return(recordWorkflowTaskStartedResponse, nil).AnyTimes()
_, syncMatch, err := s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
s.Equal(syncMatch, expectSyncMatch)
if expectedError != nil {
s.ErrorAs(err, &expectedError)
} else {
s.Nil(err)
}
wg.Wait()
}
func (s *matchingEngineSuite) TestPollWorkflowTaskQueues() {
namespaceID := namespace.ID(uuid.NewString())
tl := "makeToast"
stickyTl := "makeStickyToast"
stickyTlKind := enumspb.TASK_QUEUE_KIND_STICKY
identity := "selfDrivingToaster"
stickyTaskQueue := &taskqueuepb.TaskQueue{Name: stickyTl, Kind: stickyTlKind}
s.matchingEngine.config.RangeSize = 2 // to test that range is not updated without tasks
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(10 * time.Millisecond)
runID := uuid.NewString()
workflowID := "workflow1"
workflowType := &commonpb.WorkflowType{
Name: "workflow",
}
execution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
scheduledEventID := int64(0)
// History service is using mock
s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...any) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
s.logger.Debug("Mock Received RecordWorkflowTaskStartedRequest")
response := &historyservice.RecordWorkflowTaskStartedResponse{
WorkflowType: workflowType,
PreviousStartedEventId: scheduledEventID,
ScheduledEventId: scheduledEventID + 1,
Attempt: 1,
StickyExecutionEnabled: true,
WorkflowExecutionTaskQueue: &taskqueuepb.TaskQueue{Name: tl, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
History: &historypb.History{Events: []*historypb.HistoryEvent{}},
NextPageToken: nil,
}
return response, nil
}).AnyTimes()
addRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID.String(),
Execution: execution,
ScheduledEventId: scheduledEventID,
TaskQueue: stickyTaskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
_, _, err := s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
// fail due to no sticky worker
s.ErrorAs(err, new(*serviceerrors.StickyWorkerUnavailable))
// poll the sticky queue, should get no result
resp, err := s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID.String(),
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: stickyTaskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
s.NoError(err)
s.Equal(emptyPollWorkflowTaskQueueResponse, resp)
// add task to sticky queue again, this time it should pass
_, _, err = s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
s.NoError(err)
resp, err = s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID.String(),
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: stickyTaskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
s.NoError(err)
expectedResp := &matchingservice.PollWorkflowTaskQueueResponseWithRawHistory{
TaskToken: resp.TaskToken,
WorkflowExecution: execution,
WorkflowType: workflowType,
PreviousStartedEventId: scheduledEventID,
StartedEventId: common.EmptyEventID,
Attempt: 1,
NextEventId: common.EmptyEventID,
BacklogCountHint: 0,
StickyExecutionEnabled: true,
Query: nil,
TransientWorkflowTask: nil,
WorkflowExecutionTaskQueue: &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
BranchToken: nil,
ScheduledTime: nil,
StartedTime: nil,
Queries: nil,
History: &historypb.History{Events: []*historypb.HistoryEvent{}},
NextPageToken: nil,
}
s.Nil(err)
s.Equal(expectedResp, resp)
}
func (s *matchingEngineSuite) TestPollWorkflowTaskQueues_NamespaceHandover() {
namespaceID := uuid.NewString()
taskQueue := &taskqueuepb.TaskQueue{Name: "queue", Kind: enumspb.TASK_QUEUE_KIND_NORMAL}
addRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{WorkflowId: "workflowID", RunId: uuid.NewString()},
ScheduledEventId: int64(0),
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
// add multiple workflow tasks, but matching should not keep polling new tasks
// upon getting namespace handover error when recording start for the first task
_, _, err := s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
s.NoError(err)
_, _, err = s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
s.NoError(err)
s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, common.ErrNamespaceHandover).Times(1)
resp, err := s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: taskQueue,
Identity: "identity",
},
}, metrics.NoopMetricsHandler)
s.Nil(resp)
s.Equal(common.ErrNamespaceHandover.Error(), err.Error())
}
func (s *matchingEngineSuite) TestPollActivityTaskQueues_InternalError() {
s.logger.Expect(testlogger.Error, "dropping task due to non-nonretryable errors")
namespaceID := uuid.NewString()
tl := "queue"
taskQueue := &taskqueuepb.TaskQueue{Name: "queue", Kind: enumspb.TASK_QUEUE_KIND_NORMAL}
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{WorkflowId: "workflowID", RunId: uuid.NewString()},
ScheduledEventId: int64(5),
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(0),
}
// add an activity task
_, _, err := s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
s.NoError(err)
s.Equal(1, s.taskManager.getTaskCount(newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)))
// task is dropped with no retry; RecordActivityTaskStarted should only be called once
s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, serviceerror.NewInternal("Internal error")).Times(1)
resp, err := s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: "identity",
},
}, metrics.NoopMetricsHandler)
protoassert.ProtoEqual(s.T(), emptyPollActivityTaskQueueResponse, resp)
s.NoError(err)
}
func (s *matchingEngineSuite) TestPollActivityTaskQueues_DataLossError() {
s.logger.Expect(testlogger.Error, "dropping task due to non-nonretryable errors")
namespaceID := uuid.NewString()
tl := "queue"
taskQueue := &taskqueuepb.TaskQueue{Name: "queue", Kind: enumspb.TASK_QUEUE_KIND_NORMAL}
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{WorkflowId: "workflowID", RunId: uuid.NewString()},
ScheduledEventId: int64(5),
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(0),
}
// add an activity task
_, _, err := s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
s.NoError(err)
s.Equal(1, s.taskManager.getTaskCount(newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)))
// task is dropped with no retry; RecordActivityTaskStarted should only be called once
s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, serviceerror.NewDataLoss("DataLoss Error")).Times(1)
resp, err := s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: "identity",
},
}, metrics.NoopMetricsHandler)
protoassert.ProtoEqual(s.T(), emptyPollActivityTaskQueueResponse, resp)
s.NoError(err)
}
func (s *matchingEngineSuite) TestPollWorkflowTaskQueues_InternalError() {
s.logger.Expect(testlogger.Error, "dropping task due to non-nonretryable errors")
tqName := "queue"
taskQueue := &taskqueuepb.TaskQueue{Name: tqName, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}
wfExecution := &commonpb.WorkflowExecution{WorkflowId: "workflowID", RunId: uuid.NewString()}
// add a wf task
s.addWorkflowTask(wfExecution, taskQueue)
s.Equal(1, s.taskManager.getTaskCount(newUnversionedRootQueueKey(namespaceID, tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW)))
// task is dropped with no retry; RecordWorkflowTaskStarted should only be called once
s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, serviceerror.NewInternal("internal error")).Times(1)
resp, err := s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: taskQueue,
Identity: "identity",
},
}, metrics.NoopMetricsHandler)
protoassert.ProtoEqual(s.T(), emptyPollWorkflowTaskQueueResponse, resp)
s.NoError(err)
}
func (s *matchingEngineSuite) TestPollWorkflowTaskQueues_DataLossError() {
s.logger.Expect(testlogger.Error, "dropping task due to non-nonretryable errors")
tqName := "queue"
taskQueue := &taskqueuepb.TaskQueue{Name: tqName, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}
wfExecution := &commonpb.WorkflowExecution{WorkflowId: "workflowID", RunId: uuid.NewString()}
// add a wf task
s.addWorkflowTask(wfExecution, taskQueue)
s.Equal(1, s.taskManager.getTaskCount(newUnversionedRootQueueKey(namespaceID, tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW)))
// task is dropped with no retry; RecordWorkflowTaskStarted should only be called once
s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, serviceerror.NewDataLoss("DataLoss error")).Times(1)
resp, err := s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: taskQueue,
Identity: "identity",
},
}, metrics.NoopMetricsHandler)
protoassert.ProtoEqual(s.T(), emptyPollWorkflowTaskQueueResponse, resp)
s.NoError(err)
}
func (s *matchingEngineSuite) TestPollActivityTaskQueues_NamespaceHandover() {
namespaceID := uuid.NewString()
taskQueue := &taskqueuepb.TaskQueue{Name: "queue", Kind: enumspb.TASK_QUEUE_KIND_NORMAL}
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{WorkflowId: "workflowID", RunId: uuid.NewString()},
ScheduledEventId: int64(5),
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
// add multiple activity tasks, but matching should not keep polling new tasks
// upon getting namespace handover error when recording start for the first task
_, _, err := s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
s.NoError(err)
_, _, err = s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
s.NoError(err)
s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, common.ErrNamespaceHandover).Times(1)
resp, err := s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: "identity",
},
}, metrics.NoopMetricsHandler)
s.Nil(resp)
s.Equal(common.ErrNamespaceHandover.Error(), err.Error())
}
func (s *matchingEngineSuite) TestAddActivityTasks() {
s.AddTasksTest(enumspb.TASK_QUEUE_TYPE_ACTIVITY, false)
}
func (s *matchingEngineSuite) TestAddWorkflowTasks() {
s.AddTasksTest(enumspb.TASK_QUEUE_TYPE_WORKFLOW, false)
}
func (s *matchingEngineSuite) TestAddWorkflowTasksForwarded() {
s.AddTasksTest(enumspb.TASK_QUEUE_TYPE_WORKFLOW, true)
}
func (s *matchingEngineSuite) TestAddWorkflowAutoEnable() {
tv := testvars.New(s.T()).WithNamespaceID(s.ns.ID())
req := &matchingservice.UpdateFairnessStateRequest{
NamespaceId: tv.NamespaceID().String(),
TaskQueue: tv.TaskQueue().Name,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
FairnessState: enumsspb.FAIRNESS_STATE_V2,
}
var didUpdate atomic.Bool
s.mockMatchingClient.EXPECT().UpdateFairnessState(context.Background(), req).DoAndReturn(
func(ctx context.Context, req *matchingservice.UpdateFairnessStateRequest, opts ...grpc.CallOption) (*matchingservice.UpdateFairnessStateResponse, error) {
didUpdate.Store(true)
return s.matchingEngine.UpdateFairnessState(ctx, req)
},
)
dbq := newUnversionedRootQueueKey(tv.NamespaceID().String(), tv.TaskQueue().Name, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
mgr := s.newPartitionManager(dbq.partition, s.matchingEngine.config)
cMgr := mgr.(*taskQueuePartitionManagerImpl)
mgr.GetUserDataManager().(*mockUserDataManager).onChange = cMgr.userDataChanged
s.matchingEngine.updateTaskQueue(dbq.partition, mgr)
mgr.Start()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
err := mgr.WaitUntilInitialized(ctx)
s.Require().NoError(err)
cancel()
_, _, err = s.matchingEngine.AddWorkflowTask(
context.Background(),
&matchingservice.AddWorkflowTaskRequest{
NamespaceId: tv.NamespaceID().String(),
Execution: tv.WorkflowExecution(),
TaskQueue: tv.TaskQueue(),
Priority: &commonpb.Priority{
PriorityKey: 3,
FairnessKey: "myFairnessKey",
},
},
)
// The task may or may not be enqueued before a shutdown, so ignore that error
if err != errShutdown {
s.Require().NoError(err)
}
s.Eventually(didUpdate.Load, time.Second, time.Millisecond)
// We check the old partition manager for the change because a new partition created will not reference the same mockUserDataManager.
data, _, _ := mgr.GetUserDataManager().GetUserData()
s.Require().Equal(enumsspb.FAIRNESS_STATE_V2, data.GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].FairnessState)
// At this point the partition manager should be unloaded
select {
case <-cMgr.initCtx.Done():
case <-time.After(time.Second):
s.Require().Fail("our partition manager was not unloaded")
}
}
func (s *matchingEngineSuite) TestSkipAutoEnable() {
if !s.newMatcher && !s.fairness {
s.T().Skip("We only skip auto enable if new matcher is explicitly enabled already")
}
// Explicitly set to zero times in the event this call is added as expected during setup in the future
s.mockMatchingClient.EXPECT().UpdateFairnessState(context.Background(), nil).DoAndReturn(
func(ctx context.Context, req *matchingservice.UpdateFairnessStateRequest, opts ...grpc.CallOption) (*matchingservice.UpdateFairnessStateResponse, error) {
return s.matchingEngine.UpdateFairnessState(ctx, req)
},
).Times(0)
tv := testvars.New(s.T())
_, _, err := s.matchingEngine.AddWorkflowTask(
context.Background(),
&matchingservice.AddWorkflowTaskRequest{
NamespaceId: tv.NamespaceID().String(),
Execution: tv.WorkflowExecution(),
TaskQueue: tv.TaskQueue(),
Priority: &commonpb.Priority{
PriorityKey: 3,
},
},
)
s.Require().NoError(err)
}
func (s *matchingEngineSuite) AddTasksTest(taskType enumspb.TaskQueueType, isForwarded bool) {
s.matchingEngine.config.RangeSize = 300 // override to low number for the test
namespaceID := s.ns.ID().String()
tl := "makeToast"
forwardedFrom := "/_sys/makeToast/1"
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
const taskCount = 111
runID := uuid.NewString()
workflowID := "workflow1"
execution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
for i := range int64(taskCount) {
scheduledEventID := i * 3
var err error
if taskType == enumspb.TASK_QUEUE_TYPE_ACTIVITY {
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: execution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
if isForwarded {
addRequest.ForwardInfo = &taskqueuespb.TaskForwardInfo{SourcePartition: forwardedFrom}
}
_, _, err = s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
} else {
addRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID,
Execution: execution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
if isForwarded {
addRequest.ForwardInfo = &taskqueuespb.TaskForwardInfo{SourcePartition: forwardedFrom}
}
_, _, err = s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
}
switch isForwarded {
case false:
s.NoError(err)
case true:
s.Equal(errRemoteSyncMatchFailed, err)
}
}
switch isForwarded {
case false:
s.Equal(taskCount, s.taskManager.getTaskCount(newUnversionedRootQueueKey(namespaceID, tl, taskType)))
case true:
s.Equal(0, s.taskManager.getTaskCount(newUnversionedRootQueueKey(namespaceID, tl, taskType)))
}
}
func (s *matchingEngineSuite) TestAddWorkflowTaskDoesNotLoadSticky() {
addRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: uuid.NewString(),
Execution: &commonpb.WorkflowExecution{RunId: uuid.NewString(), WorkflowId: "wf1"},
ScheduledEventId: 0,
TaskQueue: &taskqueuepb.TaskQueue{Name: "sticky", Kind: enumspb.TASK_QUEUE_KIND_STICKY},
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
_, _, err := s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
s.ErrorAs(err, new(*serviceerrors.StickyWorkerUnavailable))
// check loaded queues
s.matchingEngine.partitionsLock.RLock()
defer s.matchingEngine.partitionsLock.RUnlock()
s.Equal(0, len(s.matchingEngine.partitions))
}
func (s *matchingEngineSuite) TestQueryWorkflowDoesNotLoadSticky() {
query := matchingservice.QueryWorkflowRequest{
NamespaceId: uuid.NewString(),
TaskQueue: &taskqueuepb.TaskQueue{Name: "sticky", Kind: enumspb.TASK_QUEUE_KIND_STICKY},
QueryRequest: &workflowservice.QueryWorkflowRequest{
Namespace: "ns",
Execution: &commonpb.WorkflowExecution{RunId: uuid.NewString(), WorkflowId: "wf1"},
Query: &querypb.WorkflowQuery{QueryType: "q"},
},
}
_, err := s.matchingEngine.QueryWorkflow(context.Background(), &query)
s.ErrorAs(err, new(*serviceerrors.StickyWorkerUnavailable))
// check loaded queues
s.matchingEngine.partitionsLock.RLock()
defer s.matchingEngine.partitionsLock.RUnlock()
s.Equal(0, len(s.matchingEngine.partitions))
}
func (s *matchingEngineSuite) TestAddThenConsumeActivities() {
if s.newMatcher {
s.T().Skip("not supported by new matcher; flaky")
}
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(10 * time.Millisecond)
runID := uuid.NewString()
workflowID := "workflow1"
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
const taskCount = 1000
// TODO: Understand why publish is low when rangeSize is 3
const rangeSize = 30
namespaceID := uuid.NewString()
tl := "makeToast"
tlID := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
s.matchingEngine.config.RangeSize = rangeSize // override to low number for the test
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
for i := range int64(taskCount) {
scheduledEventID := i * 3
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
_, _, err := s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
s.NoError(err)
}
s.EqualValues(taskCount, s.taskManager.getTaskCount(tlID))
activityTypeName := "activity1"
activityID := "activityId1"
activityType := &commonpb.ActivityType{Name: activityTypeName}
activityInput := payloads.EncodeString("Activity1 Input")
identity := "nobody"
// History service is using mock
s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) {
s.logger.Debug("Mock Received RecordActivityTaskStartedRequest")
resp := &historyservice.RecordActivityTaskStartedResponse{
Attempt: 1,
ScheduledEvent: newActivityTaskScheduledEvent(taskRequest.ScheduledEventId, 0,
&commandpb.ScheduleActivityTaskCommandAttributes{
ActivityId: activityID,
TaskQueue: &taskqueuepb.TaskQueue{
Name: taskQueue.Name,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
ActivityType: activityType,
Input: activityInput,
ScheduleToCloseTimeout: durationpb.New(100 * time.Second),
ScheduleToStartTimeout: durationpb.New(50 * time.Second),
StartToCloseTimeout: durationpb.New(50 * time.Second),
HeartbeatTimeout: durationpb.New(10 * time.Second),
}),
}
resp.StartedTime = timestamp.TimeNowPtrUtc()
return resp, nil
}).AnyTimes()
for i := int64(0); i < taskCount; {
scheduledEventID := i * 3
result, err := s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
s.NoError(err)
s.NotNil(result)
if len(result.TaskToken) == 0 {
s.logger.Debug("empty poll returned")
continue
}
s.EqualValues(activityID, result.ActivityId)
s.EqualValues(activityType, result.ActivityType)
s.EqualValues(activityInput, result.Input)
s.EqualValues(workflowExecution, result.WorkflowExecution)
s.Equal(true, validateTimeRange(result.ScheduledTime.AsTime(), time.Minute))
s.EqualValues(time.Second*100, result.ScheduleToCloseTimeout.AsDuration())
s.Equal(true, validateTimeRange(result.StartedTime.AsTime(), time.Minute))
s.EqualValues(time.Second*50, result.StartToCloseTimeout.AsDuration())
s.EqualValues(time.Second*10, result.HeartbeatTimeout.AsDuration())
taskToken := &tokenspb.Task{
Attempt: 1,
NamespaceId: namespaceID,
WorkflowId: workflowID,
RunId: runID,
ScheduledEventId: scheduledEventID,
ActivityId: activityID,
ActivityType: activityTypeName,
}
serializedToken, _ := s.matchingEngine.tokenSerializer.Serialize(taskToken)
s.EqualValues(serializedToken, result.TaskToken)
i++
}
s.EqualValues(0, s.taskManager.getTaskCount(tlID))
expectedRange := int64((taskCount + 1) / rangeSize)
// Due to conflicts some ids are skipped and more real ranges are used.
s.LessOrEqual(expectedRange, s.taskManager.getQueueDataByKey(tlID).rangeID)
}
// TODO: this unit test does not seem to belong to matchingEngine, move it to the right place
func (s *matchingEngineSuite) TestSyncMatchActivities() {
if s.newMatcher {
s.T().Skip("not supported by new matcher")
}
s.logger.Expect(testlogger.Error, "unexpected error dispatching task")
scope := tally.NewTestScope("test", nil)
s.matchingEngine.metricsHandler = metrics.NewTallyMetricsHandler(metrics.ClientConfig{}, scope).WithTags(metrics.ServiceNameTag(primitives.MatchingService))
// Set a short long poll expiration so that we don't have to wait too long for 0 throttling cases
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(2 * time.Second)
s.matchingEngine.config.MinTaskThrottlingBurstSize = dynamicconfig.GetIntPropertyFnFilteredByTaskQueue(0)
s.matchingEngine.config.RangeSize = 30 // override to low number for the test
// Overriding the dynamic config so that the rate-limiter has a refresh rate of 0. By default, the rate-limiter has a refresh rate of 1 minute which is too long for this test.
s.matchingEngine.config.RateLimiterRefreshInterval = 0
s.matchingEngine.config.AdminNamespaceToPartitionDispatchRate = dynamicconfig.GetFloatPropertyFnFilteredByNamespace(25000)
s.matchingEngine.config.AdminNamespaceTaskqueueToPartitionDispatchRate = dynamicconfig.GetFloatPropertyFnFilteredByTaskQueue(25000)
namespaceID := uuid.NewString()
tl := "makeToast"
dbq := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
mgr := s.newPartitionManager(dbq.partition, s.matchingEngine.config)
// Directly override admin rate limits to simulate dynamic config at rateLimitManager.
mgr.GetRateLimitManager().SetAdminRateForTesting(25000.0)
s.matchingEngine.updateTaskQueue(dbq.partition, mgr)
mgr.Start()
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
activityTypeName := "activity1"
activityID := "activityId1"
activityType := &commonpb.ActivityType{Name: activityTypeName}
activityInput := payloads.EncodeString("Activity1 Input")
identity := "nobody"
s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) {
s.logger.Debug("Mock Received RecordActivityTaskStartedRequest")
return &historyservice.RecordActivityTaskStartedResponse{
Attempt: 1,
ScheduledEvent: newActivityTaskScheduledEvent(taskRequest.ScheduledEventId, 0,
&commandpb.ScheduleActivityTaskCommandAttributes{
ActivityId: activityID,
TaskQueue: &taskqueuepb.TaskQueue{
Name: taskQueue.Name,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
ActivityType: activityType,
Input: activityInput,
ScheduleToStartTimeout: durationpb.New(1 * time.Second),
ScheduleToCloseTimeout: durationpb.New(2 * time.Second),
StartToCloseTimeout: durationpb.New(1 * time.Second),
HeartbeatTimeout: durationpb.New(1 * time.Second),
}),
}, nil
}).AnyTimes()
const taskCount = 10
runID := uuid.NewString()
workflowID := "workflow1"
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
pollFunc := func(maxDispatch float64) (*matchingservice.PollActivityTaskQueueResponse, error) {
return s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
TaskQueueMetadata: &taskqueuepb.TaskQueueMetadata{MaxTasksPerSecond: &wrapperspb.DoubleValue{Value: maxDispatch}},
},
}, metrics.NoopMetricsHandler)
}
for i := range int64(taskCount) {
scheduledEventID := i * 3
var wg sync.WaitGroup
var result *matchingservice.PollActivityTaskQueueResponse
var pollErr error
maxDispatch := defaultTaskDispatchRPS
if i == taskCount/2 {
maxDispatch = 0
}
wg.Add(1)
go func() {
defer wg.Done()
result, pollErr = pollFunc(maxDispatch)
}()
time.Sleep(20 * time.Millisecond) // Necessary for sync match to happen
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
_, _, err := s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
wg.Wait()
s.NoError(err)
s.NoError(pollErr)
s.NotNil(result)
if len(result.TaskToken) == 0 {
// when ratelimit is set to zero, poller is expected to return empty result
// reset ratelimit, poll again and make sure task is returned this time
s.logger.Debug("empty poll returned")
s.Equal(float64(0), maxDispatch)
maxDispatch = defaultTaskDispatchRPS
wg.Add(1)
go func() {
defer wg.Done()
result, pollErr = pollFunc(maxDispatch)
}()
wg.Wait()
s.NoError(err)
s.NoError(pollErr)
s.NotNil(result)
s.True(len(result.TaskToken) > 0)
}
s.EqualValues(activityID, result.ActivityId)
s.EqualValues(activityType, result.ActivityType)
s.EqualValues(activityInput, result.Input)
s.EqualValues(workflowExecution, result.WorkflowExecution)
taskToken := &tokenspb.Task{
Attempt: 1,
NamespaceId: namespaceID,
WorkflowId: workflowID,
RunId: runID,
ScheduledEventId: scheduledEventID,
ActivityId: activityID,
ActivityType: activityTypeName,
}
serializedToken, _ := s.matchingEngine.tokenSerializer.Serialize(taskToken)
s.EqualValues(serializedToken, result.TaskToken)
}
s.EventuallyWithT(func(collect *assert.CollectT) {
assert.EqualValues(collect, 1, s.taskManager.getCreateTaskCount(dbq)) // Check times zero rps is set = Tasks stored in persistence
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_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.
s.LessOrEqual(expectedRange, s.taskManager.getQueueDataByKey(dbq).rangeID)
// check the poller information
tlType := enumspb.TASK_QUEUE_TYPE_ACTIVITY
descResp, err := s.matchingEngine.DescribeTaskQueue(context.Background(), &matchingservice.DescribeTaskQueueRequest{
NamespaceId: namespaceID,
DescRequest: &workflowservice.DescribeTaskQueueRequest{
TaskQueue: taskQueue,
TaskQueueType: tlType,
IncludeTaskQueueStatus: true,
},
})
s.NoError(err)
s.Equal(1, len(descResp.DescResponse.Pollers))
s.Equal(identity, descResp.DescResponse.Pollers[0].GetIdentity())
s.NotEmpty(descResp.DescResponse.Pollers[0].GetLastAccessTime())
s.Equal(defaultTaskDispatchRPS, descResp.DescResponse.Pollers[0].GetRatePerSecond())
s.NotNil(descResp.DescResponse.GetTaskQueueStatus())
numPartitions := float64(s.matchingEngine.config.NumTaskqueueWritePartitions("", "", tlType))
//nolint:staticcheck // checking deprecated field
s.GreaterOrEqual(descResp.DescResponse.GetTaskQueueStatus().GetRatePerSecond()*numPartitions,
(defaultTaskDispatchRPS - 1))
}
func (s *matchingEngineSuite) TestRateLimiterAcrossVersionedQueues() {
/*
1. Start a versioned poller with maxTasksPerSecond = defaultTaskDispatchRPS
2. Start another versioned poller, polling a different version, maxTasksPerSecond = 0
3. Add tasks to both these versioned queues and notice no dispatch of tasks.
4. Restart the pollers with maxTasksPerSecond = defaultTaskDispatchRPS
5. Verify that both the pollers have received tasks.
*/
if s.newMatcher {
s.T().Skip("not supported by new matcher")
}
s.logger.Expect(testlogger.Error, "unexpected error dispatching task")
scope := tally.NewTestScope("test", nil)
s.matchingEngine.metricsHandler = metrics.NewTallyMetricsHandler(metrics.ClientConfig{}, scope).WithTags(metrics.ServiceNameTag(primitives.MatchingService))
// Set a short long poll expiration so that the pollers don't wait too long for tasks
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(5 * time.Second)
s.matchingEngine.config.MinTaskThrottlingBurstSize = dynamicconfig.GetIntPropertyFnFilteredByTaskQueue(0)
// Disable deployment versions since a nil DeploymentClient is used in unit tests
s.matchingEngine.config.EnableDeploymentVersions = dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false)
// Overriding the dynamic config so that the rate-limiter has a refresh rate of 0. By default, the rate-limiter has a refresh rate of 1 minute which is too long for this test.
s.matchingEngine.config.RateLimiterRefreshInterval = 0
tl := "makeToast"
dbq := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
runID := uuid.NewString()
workflowID := "workflow1"
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
deploymentName := "test-deployment"
mgr := s.newPartitionManager(dbq.partition, s.matchingEngine.config)
tqPTM, ok := mgr.(*taskQueuePartitionManagerImpl)
s.True(ok)
s.matchingEngine.updateTaskQueue(dbq.partition, mgr)
mgr.Start()
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
activityTypeName := "activity1"
activityID := "activityId1"
activityType := &commonpb.ActivityType{Name: activityTypeName}
activityInput := payloads.EncodeString("Activity1 Input")
identity := "nobody"
s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) {
s.logger.Debug("Mock Received RecordActivityTaskStartedRequest")
return &historyservice.RecordActivityTaskStartedResponse{
Attempt: 1,
ScheduledEvent: newActivityTaskScheduledEvent(taskRequest.ScheduledEventId, 0,
&commandpb.ScheduleActivityTaskCommandAttributes{
ActivityId: activityID,
TaskQueue: &taskqueuepb.TaskQueue{
Name: taskQueue.Name,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
ActivityType: activityType,
Input: activityInput,
ScheduleToStartTimeout: durationpb.New(1 * time.Second),
ScheduleToCloseTimeout: durationpb.New(2 * time.Second),
StartToCloseTimeout: durationpb.New(1 * time.Second),
HeartbeatTimeout: durationpb.New(1 * time.Second),
}),
}, nil
}).Times(2)
pollFunc := func(maxDispatch float64, buildID string) (*matchingservice.PollActivityTaskQueueResponse, error) {
return s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
TaskQueueMetadata: &taskqueuepb.TaskQueueMetadata{MaxTasksPerSecond: &wrapperspb.DoubleValue{Value: maxDispatch}},
DeploymentOptions: &deploymentpb.WorkerDeploymentOptions{
DeploymentName: deploymentName,
BuildId: buildID,
WorkerVersioningMode: enumspb.WORKER_VERSIONING_MODE_VERSIONED,
},
},
}, metrics.NoopMetricsHandler)
}
const taskCount = 2
resultChan := make(chan *matchingservice.PollActivityTaskQueueResponse)
for i := range int64(taskCount) {
maxDispatch := defaultTaskDispatchRPS
if i == 1 {
maxDispatch = 0 // second poller overrides the dispatch rate to 0
}
go func() {
result, _ := pollFunc(float64(maxDispatch), strconv.FormatInt(int64(i), 10))
resultChan <- result
}()
//nolint:forbidigo
time.Sleep(10 * time.Millisecond) // Delay to allow the second poller coming in a little later.
}
// Update user data of the task queue so that the activity tasks generated are not treated as independent activities.
// Independent activity tasks are those if the task queue the task is scheduled on is not part of the workflow's pinned
// deployment.
updateOptions := UserDataUpdateOptions{Source: "SyncDeploymentUserData"}
_, err := tqPTM.GetUserDataManager().UpdateUserData(context.Background(), updateOptions, func(data *persistencespb.TaskQueueUserData) (*persistencespb.TaskQueueUserData, bool, error) {
newData := &persistencespb.TaskQueueUserData{
PerType: map[int32]*persistencespb.TaskQueueTypeUserData{
int32(enumspb.TASK_QUEUE_TYPE_ACTIVITY): {
DeploymentData: &persistencespb.DeploymentData{
Versions: []*deploymentspb.DeploymentVersionData{
{
Version: &deploymentspb.WorkerDeploymentVersion{
DeploymentName: deploymentName,
BuildId: strconv.FormatInt(int64(0), 10),
},
RoutingUpdateTime: timestamppb.Now(),
CurrentSinceTime: timestamppb.Now(),
},
{
Version: &deploymentspb.WorkerDeploymentVersion{
DeploymentName: deploymentName,
BuildId: strconv.FormatInt(int64(1), 10),
},
RoutingUpdateTime: timestamppb.Now(),
RampingSinceTime: timestamppb.Now(),
},
},
},
},
},
}
return newData, true, nil
})
s.NoError(err)
for i := range int64(taskCount) {
scheduledEventID := i * 3
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
VersionDirective: &taskqueuespb.TaskVersionDirective{
Behavior: enumspb.VERSIONING_BEHAVIOR_PINNED,
DeploymentVersion: &deploymentspb.WorkerDeploymentVersion{
DeploymentName: deploymentName,
BuildId: strconv.FormatInt(int64(i), 10),
},
},
}
_, _, err = s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
s.NoError(err)
}
// Verifying that both the pollers don't receive any tasks since the overall dispatch rate has been set to 0
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 := range int64(taskCount) {
go func() {
result, _ := pollFunc(maxDispatch, strconv.FormatInt(int64(i), 10))
resultChan <- result
}()
}
// Verifying that both the pollers receive the tasks which were added previously
for range int64(taskCount) {
receivedResult := <-resultChan
s.NotNil(receivedResult)
s.NotNil(receivedResult.TaskToken)
}
}
func (s *matchingEngineSuite) TestConcurrentPublishConsumeActivities() {
if s.newMatcher {
s.T().Skip("test is flaky with new matcher")
}
dispatchLimitFn := func(int, int64) float64 {
return defaultTaskDispatchRPS
}
const workerCount = 20
const taskCount = 100
throttleCt := s.concurrentPublishConsumeActivities(workerCount, taskCount, dispatchLimitFn)
s.Zero(throttleCt)
}
func (s *matchingEngineSuite) TestConcurrentPublishConsumeActivitiesWithZeroDispatch() {
s.T().Skip("Racy - times out ~50% of the time running locally with --race")
// Set a short long poll expiration so that we don't have to wait too long for 0 throttling cases
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(20 * time.Millisecond)
dispatchLimitFn := func(wc int, tc int64) float64 {
if tc%50 == 0 && wc%5 == 0 { // Gets triggered atleast 20 times
return 0
}
return defaultTaskDispatchRPS
}
const workerCount = 20
const taskCount = 100
throttleCt := s.concurrentPublishConsumeActivities(workerCount, taskCount, dispatchLimitFn)
s.logger.Info("Number of tasks throttled", tag.Number(throttleCt))
// atleast once from 0 dispatch poll, and until TTL is hit at which time throttle limit is reset
// hard to predict exactly how many times, since the atomic.Value load might not have updated.
s.GreaterOrEqual(throttleCt, 1)
}
func (s *matchingEngineSuite) concurrentPublishConsumeActivities(
workerCount int,
taskCount int64,
dispatchLimitFn func(int, int64) float64,
) int64 {
scope := tally.NewTestScope("test", nil)
s.matchingEngine.metricsHandler = metrics.NewTallyMetricsHandler(metrics.ClientConfig{}, scope).WithTags(metrics.ServiceNameTag(primitives.MatchingService))
s.matchingEngine.config.MinTaskThrottlingBurstSize = dynamicconfig.GetIntPropertyFnFilteredByTaskQueue(0)
runID := uuid.NewString()
workflowID := "workflow1"
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
const rangeSize = 3
var scheduledEventID int64 = 123
namespaceID := uuid.NewString()
tl := "makeToast"
dbq := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
s.matchingEngine.config.RangeSize = rangeSize // override to low number for the test
mgr := s.newPartitionManager(dbq.partition, s.matchingEngine.config)
s.matchingEngine.updateTaskQueue(dbq.partition, mgr)
mgr.Start()
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
var wg sync.WaitGroup
wg.Add(2 * workerCount)
for range workerCount {
go func() {
defer wg.Done()
for i := int64(0); i < taskCount; i++ {
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
_, _, err := s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
if err != nil {
s.logger.Info("Failure in AddActivityTask", tag.Error(err))
i--
}
}
}()
}
activityTypeName := "activity1"
activityID := "activityId1"
activityType := &commonpb.ActivityType{Name: activityTypeName}
activityInput := payloads.EncodeString("Activity1 Input")
activityHeader := &commonpb.Header{
Fields: map[string]*commonpb.Payload{"tracing": payload.EncodeString("tracing data")},
}
identity := "nobody"
// History service is using mock
s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) {
s.logger.Debug("Mock Received RecordActivityTaskStartedRequest")
return &historyservice.RecordActivityTaskStartedResponse{
Attempt: 1,
ScheduledEvent: newActivityTaskScheduledEvent(taskRequest.ScheduledEventId, 0,
&commandpb.ScheduleActivityTaskCommandAttributes{
ActivityId: activityID,
TaskQueue: &taskqueuepb.TaskQueue{
Name: taskQueue.Name,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
ActivityType: activityType,
Input: activityInput,
Header: activityHeader,
ScheduleToStartTimeout: durationpb.New(1 * time.Second),
ScheduleToCloseTimeout: durationpb.New(2 * time.Second),
StartToCloseTimeout: durationpb.New(1 * time.Second),
HeartbeatTimeout: durationpb.New(1 * time.Second),
}),
}, nil
}).AnyTimes()
for p := range workerCount {
go func(wNum int) {
defer wg.Done()
for i := int64(0); i < taskCount; {
maxDispatch := dispatchLimitFn(wNum, i)
result, err := s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
TaskQueueMetadata: &taskqueuepb.TaskQueueMetadata{MaxTasksPerSecond: &wrapperspb.DoubleValue{Value: maxDispatch}},
},
}, metrics.NoopMetricsHandler)
s.NoError(err)
s.NotNil(result)
if len(result.TaskToken) == 0 {
s.logger.Debug("empty poll returned")
continue
}
s.EqualValues(activityID, result.ActivityId)
s.EqualValues(activityType, result.ActivityType)
s.EqualValues(activityInput, result.Input)
s.EqualValues(activityHeader, result.Header)
s.EqualValues(workflowExecution, result.WorkflowExecution)
taskToken := &tokenspb.Task{
Attempt: 1,
NamespaceId: namespaceID,
WorkflowId: workflowID,
RunId: runID,
ScheduledEventId: scheduledEventID,
ActivityId: activityID,
ActivityType: activityTypeName,
}
resultToken, err := s.matchingEngine.tokenSerializer.Deserialize(result.TaskToken)
s.NoError(err)
protoassert.ProtoEqual(s.T(), taskToken, resultToken)
i++
}
}(p)
}
wg.Wait()
totalTasks := int(taskCount) * workerCount
persisted := s.taskManager.getCreateTaskCount(dbq)
s.True(persisted < totalTasks)
expectedRange := int64((persisted + 1) / rangeSize)
// Due to conflicts some ids are skipped and more real ranges are used.
s.LessOrEqual(expectedRange, s.taskManager.getQueueDataByKey(dbq).rangeID)
s.EqualValues(0, s.taskManager.getTaskCount(dbq))
syncCtr := scope.Snapshot().Counters()["test.sync_throttle_count+namespace="+matchingTestNamespace+",operation=TaskQueueMgr,taskqueue=makeToast"]
bufCtr := scope.Snapshot().Counters()["test.buffer_throttle_count+namespace="+matchingTestNamespace+",operation=TaskQueueMgr,taskqueue=makeToast"]
total := int64(0)
if syncCtr != nil {
total += syncCtr.Value()
}
if bufCtr != nil {
total += bufCtr.Value()
}
return total
}
func (s *matchingEngineSuite) TestConcurrentPublishConsumeWorkflowTasks() {
if s.newMatcher {
s.T().Skip("not supported by new matcher; flaky")
}
runID := uuid.NewString()
workflowID := "workflow1"
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
const workerCount = 20
const taskCount = 100
const rangeSize = 5
var scheduledEventID int64 = 123
var startedEventID int64 = 1412
namespaceID := uuid.NewString()
tl := "makeToast"
tlID := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
s.matchingEngine.config.RangeSize = rangeSize // override to low number for the test
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
var wg sync.WaitGroup
wg.Add(2 * workerCount)
for range workerCount {
go func() {
for range int64(taskCount) {
addRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
_, _, err := s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
if err != nil {
panic(err)
}
}
wg.Done()
}()
}
workflowTypeName := "workflowType1"
workflowType := &commonpb.WorkflowType{Name: workflowTypeName}
identity := "nobody"
// History service is using mock
s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, taskRequest *historyservice.RecordWorkflowTaskStartedRequest, arg2 ...any) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
s.logger.Debug("Mock Received RecordWorkflowTaskStartedRequest")
return &historyservice.RecordWorkflowTaskStartedResponse{
PreviousStartedEventId: startedEventID,
StartedEventId: startedEventID,
ScheduledEventId: scheduledEventID,
WorkflowType: workflowType,
Attempt: 1,
History: &historypb.History{Events: []*historypb.HistoryEvent{}},
NextPageToken: nil,
}, nil
}).AnyTimes()
for range workerCount {
go func() {
for i := int64(0); i < taskCount; {
result, err := s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
if err != nil {
panic(err)
}
s.NotNil(result)
if len(result.TaskToken) == 0 {
s.logger.Debug("empty poll returned")
continue
}
s.EqualValues(workflowExecution, result.WorkflowExecution)
s.EqualValues(workflowType, result.WorkflowType)
s.EqualValues(startedEventID, result.StartedEventId)
s.EqualValues(workflowExecution, result.WorkflowExecution)
taskToken := &tokenspb.Task{
Attempt: 1,
NamespaceId: namespaceID,
WorkflowId: workflowID,
RunId: runID,
ScheduledEventId: scheduledEventID,
StartedEventId: startedEventID,
}
resultToken, err := s.matchingEngine.tokenSerializer.Deserialize(result.TaskToken)
if err != nil {
panic(err)
}
protoassert.ProtoEqual(s.T(), taskToken, resultToken)
i++
}
wg.Done()
}()
}
wg.Wait()
s.EqualValues(0, s.taskManager.getTaskCount(tlID))
totalTasks := taskCount * workerCount
persisted := s.taskManager.getCreateTaskCount(tlID)
s.True(persisted < totalTasks)
expectedRange := int64((persisted + 1) / rangeSize)
// Due to conflicts some ids are skipped and more real ranges are used.
s.LessOrEqual(expectedRange, s.taskManager.getQueueDataByKey(tlID).rangeID)
}
func (s *matchingEngineSuite) TestPollWithExpiredContext() {
identity := "nobody"
namespaceID := namespace.ID(uuid.NewString())
tl := "makeToast"
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
// Try with cancelled context
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
cancel()
_, err := s.matchingEngine.PollActivityTaskQueue(ctx, &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID.String(),
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
s.Equal(ctx.Err(), err)
// Try with expired context
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := s.matchingEngine.PollActivityTaskQueue(ctx, &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID.String(),
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
s.Nil(err)
s.Equal(emptyPollActivityTaskQueueResponse, resp)
}
func (s *matchingEngineSuite) TestForceUnloadTaskQueue() {
s.logger.Expect(testlogger.Error, "unexpected error dispatching task", tag.Error(errTaskQueueClosed))
ctx := context.Background()
namespaceID := uuid.NewString()
identity := "nobody"
// We unload a sticky queue so that we can verify the unload took effect by
// attempting to add a task to it
stickyQueue := &taskqueuepb.TaskQueue{Name: "sticky-queue", Kind: enumspb.TASK_QUEUE_KIND_STICKY}
addTaskRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{WorkflowId: "workflowID", RunId: uuid.NewString()},
ScheduledEventId: int64(0),
TaskQueue: stickyQueue,
}
// Poll to create the queue
pollResp, err := s.matchingEngine.PollWorkflowTaskQueue(ctx, &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: stickyQueue,
Identity: identity,
}},
metrics.NoopMetricsHandler)
s.Nil(err)
s.NotNil(pollResp)
// Sanity check: adding a task should succeed with the queue loaded
_, _, err = s.matchingEngine.AddWorkflowTask(ctx, &addTaskRequest)
s.NoError(err)
// Force unload the sticky queue
unloadResp, err := s.matchingEngine.ForceUnloadTaskQueuePartition(ctx, &matchingservice.ForceUnloadTaskQueuePartitionRequest{
NamespaceId: namespaceID,
TaskQueuePartition: &taskqueuespb.TaskQueuePartition{
TaskQueue: stickyQueue.Name,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
},
})
s.NoError(err)
s.NotNil(unloadResp)
s.True(unloadResp.WasLoaded)
// Adding a task should now fast fail
_, _, err = s.matchingEngine.AddWorkflowTask(ctx, &addTaskRequest)
s.ErrorAs(err, new(*serviceerrors.StickyWorkerUnavailable))
}
func (s *matchingEngineSuite) TestMultipleEnginesActivitiesRangeStealing() {
if s.newMatcher {
s.T().Skip("test is flaky with new matcher")
}
runID := uuid.NewString()
workflowID := "workflow1"
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
const engineCount = 2
const taskCount = 400
const iterations = 2
const rangeSize = 10
var scheduledEventID int64 = 123
namespaceID := uuid.NewString()
tl := "makeToast"
tlID := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
s.matchingEngine.config.RangeSize = rangeSize // override to low number for the test
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
engines := make([]*matchingEngineImpl, engineCount)
for p := range engineCount {
e := s.newMatchingEngine(s.newConfig(), s.classicTaskManager, s.fairTaskManager)
e.config.RangeSize = rangeSize
engines[p] = e
e.Start()
}
for range iterations {
for p := range engineCount {
engine := engines[p]
for i := int64(0); i < taskCount; i++ {
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(600),
}
// scheduledEventID is the key for the workflow task, so needs a different
// scheduledEventID for each task for deduplication logic below
scheduledEventID++
_, _, err := engine.AddActivityTask(context.Background(), &addRequest)
if err != nil {
if _, ok := err.(*persistence.ConditionFailedError); ok {
i-- // retry adding
} else {
panic(fmt.Sprintf("errType=%T, err=%v", err, err))
}
}
}
}
}
s.EqualValues(iterations*engineCount*taskCount, s.taskManager.getCreateTaskCount(tlID))
activityTypeName := "activity1"
activityID := "activityId1"
activityType := &commonpb.ActivityType{Name: activityTypeName}
activityInput := payloads.EncodeString("Activity1 Input")
identity := "nobody"
startedTasks := make(map[int64]struct{})
// History service is using mock
s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
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")
}
s.logger.Debug("Mock Received RecordActivityTaskStartedRequest", tag.Int64("scheduled-event-id", taskRequest.GetScheduledEventId()))
startedTasks[taskRequest.GetScheduledEventId()] = struct{}{}
return &historyservice.RecordActivityTaskStartedResponse{
Attempt: 1,
ScheduledEvent: newActivityTaskScheduledEvent(taskRequest.ScheduledEventId, 0,
&commandpb.ScheduleActivityTaskCommandAttributes{
ActivityId: activityID,
TaskQueue: &taskqueuepb.TaskQueue{
Name: taskQueue.Name,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
ActivityType: activityType,
Input: activityInput,
ScheduleToStartTimeout: durationpb.New(600 * time.Second),
ScheduleToCloseTimeout: durationpb.New(2 * time.Second),
StartToCloseTimeout: durationpb.New(1 * time.Second),
HeartbeatTimeout: durationpb.New(1 * time.Second),
}),
}, nil
}).AnyTimes()
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{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
if err != nil {
panic(err)
}
s.NotNil(result)
if len(result.TaskToken) == 0 {
s.logger.Debug("empty poll returned")
continue
}
s.EqualValues(activityID, result.ActivityId)
s.EqualValues(activityType, result.ActivityType)
s.EqualValues(activityInput, result.Input)
s.EqualValues(workflowExecution, result.WorkflowExecution)
taskToken := &tokenspb.Task{
Attempt: 1,
NamespaceId: namespaceID,
WorkflowId: workflowID,
RunId: runID,
ActivityId: activityID,
ActivityType: activityTypeName,
}
resultToken, err := engine.tokenSerializer.Deserialize(result.TaskToken)
if err != nil {
panic(err)
}
// we don't know the expected scheduledEventID for the task polled, so just set it to the result
taskToken.ScheduledEventId = resultToken.ScheduledEventId
protoassert.ProtoEqual(s.T(), taskToken, resultToken)
i++
}
}
}
for _, e := range engines {
e.Stop()
}
s.EqualValues(0, s.taskManager.getTaskCount(tlID))
totalTasks := taskCount * engineCount * iterations
persisted := s.taskManager.getCreateTaskCount(tlID)
// No sync matching as all messages are published first
s.EqualValues(totalTasks, persisted)
expectedRange := int64((persisted + 1) / rangeSize)
// Due to conflicts some ids are skipped and more real ranges are used.
s.LessOrEqual(expectedRange, s.taskManager.getQueueDataByKey(tlID).rangeID)
}
func (s *matchingEngineSuite) TestMultipleEnginesWorkflowTasksRangeStealing() {
if s.newMatcher {
s.T().Skip("test is flaky with new matcher")
}
runID := uuid.NewString()
workflowID := "workflow1"
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
const engineCount = 2
const taskCount = 400
const iterations = 2
const rangeSize = 10
var scheduledEventID int64 = 123
namespaceID := uuid.NewString()
tl := "makeToast"
tlID := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
s.matchingEngine.config.RangeSize = rangeSize // override to low number for the test
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
engines := make([]*matchingEngineImpl, engineCount)
for p := range engineCount {
e := s.newMatchingEngine(s.newConfig(), s.classicTaskManager, s.fairTaskManager)
e.config.RangeSize = rangeSize
engines[p] = e
e.Start()
}
for range iterations {
for p := range engineCount {
engine := engines[p]
for i := int64(0); i < taskCount; i++ {
addRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(600),
}
// scheduledEventID is the key for the workflow task, so needs a different
// scheduledEventID for each task for deduplication logic below
scheduledEventID++
_, _, err := engine.AddWorkflowTask(context.Background(), &addRequest)
if err != nil {
if _, ok := err.(*persistence.ConditionFailedError); ok {
i-- // retry adding
} else {
panic(fmt.Sprintf("errType=%T, err=%v", err, err))
}
}
}
}
}
workflowTypeName := "workflowType1"
workflowType := &commonpb.WorkflowType{Name: workflowTypeName}
identity := "nobody"
var startedEventID int64 = 1412
startedTasks := make(map[int64]struct{})
// History service is using mock
s.mockHistoryClient.EXPECT().RecordWorkflowTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
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")
}
s.logger.Debug("Mock Received RecordWorkflowTaskStartedRequest", tag.Int64("scheduled-event-id", taskRequest.GetScheduledEventId()))
startedTasks[taskRequest.GetScheduledEventId()] = struct{}{}
return &historyservice.RecordWorkflowTaskStartedResponse{
PreviousStartedEventId: startedEventID,
StartedEventId: startedEventID,
ScheduledEventId: taskRequest.GetScheduledEventId(),
WorkflowType: workflowType,
Attempt: 1,
History: &historypb.History{Events: []*historypb.HistoryEvent{}},
NextPageToken: nil,
}, nil
}).AnyTimes()
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{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
if err != nil {
panic(err)
}
s.NotNil(result)
if len(result.TaskToken) == 0 {
s.logger.Debug("empty poll returned")
continue
}
s.EqualValues(workflowExecution, result.WorkflowExecution)
s.EqualValues(workflowType, result.WorkflowType)
s.EqualValues(startedEventID, result.StartedEventId)
s.EqualValues(workflowExecution, result.WorkflowExecution)
taskToken := &tokenspb.Task{
Attempt: 1,
NamespaceId: namespaceID,
WorkflowId: workflowID,
RunId: runID,
StartedEventId: startedEventID,
}
resultToken, err := engine.tokenSerializer.Deserialize(result.TaskToken)
if err != nil {
panic(err)
}
// we don't know the expected scheduledEventID for the task polled, so just set it to the result
taskToken.ScheduledEventId = resultToken.ScheduledEventId
protoassert.ProtoEqual(s.T(), taskToken, resultToken)
i++
}
}
}
for _, e := range engines {
e.Stop()
}
s.EqualValues(0, s.taskManager.getTaskCount(tlID))
totalTasks := taskCount * engineCount * iterations
persisted := s.taskManager.getCreateTaskCount(tlID)
// No sync matching as all messages are published first
s.EqualValues(totalTasks, persisted)
expectedRange := int64((persisted + 1) / rangeSize)
// Due to conflicts some ids are skipped and more real ranges are used.
s.LessOrEqual(expectedRange, s.taskManager.getQueueDataByKey(tlID).rangeID)
}
func (s *matchingEngineSuite) TestAddTaskAfterStartFailure() {
if s.newMatcher {
s.T().Skip("not supported by new matcher")
}
// test default is 100ms, but make it longer for this test so it's not flaky
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(10 * time.Second)
namespaceID := uuid.NewString()
tl := "makeToast"
dbq := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
_, _, err := s.matchingEngine.AddActivityTask(context.Background(),
&matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{RunId: uuid.NewString(), WorkflowId: "workflow1"},
ScheduledEventId: int64(0),
TaskQueue: &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
})
s.NoError(err)
s.EqualValues(1, s.taskManager.getTaskCount(dbq))
task1, _, err := s.matchingEngine.pollTask(context.Background(), dbq.partition, &pollMetadata{})
s.NoError(err)
task1.finish(serviceerror.NewInternal("test error"), true)
s.EqualValues(1, s.taskManager.getTaskCount(dbq))
task2, _, err := s.matchingEngine.pollTask(context.Background(), dbq.partition, &pollMetadata{})
s.NoError(err)
protoassert.ProtoEqual(s.T(), task1.event.Data, task2.event.Data)
s.NotEqual(task1.event.GetTaskId(), task2.event.GetTaskId(), "IDs should not match")
task2.finish(nil, true)
s.EqualValues(0, s.taskManager.getTaskCount(dbq))
}
// TODO: should be moved to backlog_manager_test
func (s *matchingEngineSuite) TestTaskQueueManagerGetTaskBatch() {
if s.newMatcher {
s.T().Skip("not supported by new matcher")
}
runID := uuid.NewString()
workflowID := "workflow1"
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
namespaceID := uuid.NewString()
tl := "makeToast"
dbq := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
const taskCount = 1200
const rangeSize = 10
s.matchingEngine.config.RangeSize = rangeSize
// add taskCount tasks
for i := range int64(taskCount) {
scheduledEventID := i * 3
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
_, _, err := s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
s.NoError(err)
}
tlMgr := s.getPhysicalTaskQueueManagerImplFromKey(dbq)
s.EqualValues(taskCount, s.taskManager.getTaskCount(dbq))
// wait until all tasks are read by the task pump and enqueued into the in-memory buffer
// at the end of this step, ackManager readLevel will also be equal to the buffer size
blm := tlMgr.backlogMgr.(*backlogManagerImpl)
expectedBufSize := min(cap(blm.taskReader.taskBuffer), taskCount)
s.Eventually(func() bool { return len(blm.taskReader.taskBuffer) == expectedBufSize },
time.Second, 5*time.Millisecond)
// unload the queue and stop all goroutines that read / write tasks in the background
// remainder of this test works with the in-memory buffer
tlMgr.UnloadFromPartitionManager(unloadCauseUnspecified)
// setReadLevel should NEVER be called without updating ackManager.outstandingTasks
// This is only for unit test purpose
blm.taskAckManager.setReadLevel(blm.getDB().GetMaxReadLevel(0))
batch, err := blm.taskReader.getTaskBatch(context.Background())
s.Nil(err)
s.EqualValues(0, len(batch.tasks))
s.EqualValues(blm.getDB().GetMaxReadLevel(0), batch.readLevel)
s.True(batch.isReadBatchDone)
blm.taskAckManager.setReadLevel(0)
batch, err = blm.taskReader.getTaskBatch(context.Background())
s.Nil(err)
s.EqualValues(rangeSize, len(batch.tasks))
s.EqualValues(rangeSize, batch.readLevel)
s.True(batch.isReadBatchDone)
s.setupRecordActivityTaskStartedMock(tl)
// reset the ackManager readLevel to the buffer size and consume
// the in-memory tasks by calling Poll API - assert ackMgr state
// at the end
blm.taskAckManager.setReadLevel(int64(expectedBufSize))
// complete rangeSize events
for range int64(rangeSize) {
identity := "nobody"
result, err := s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: identity,
},
}, metrics.NoopMetricsHandler)
s.NoError(err)
s.NotNil(result)
s.NotEqual(emptyPollActivityTaskQueueResponse, result)
if len(result.TaskToken) == 0 {
s.logger.Debug("empty poll returned")
continue
}
}
s.EqualValues(taskCount-rangeSize, s.taskManager.getTaskCount(dbq))
batch, err = blm.taskReader.getTaskBatch(context.Background())
s.Nil(err)
s.True(0 < len(batch.tasks) && len(batch.tasks) <= rangeSize)
s.True(batch.isReadBatchDone)
}
func (s *matchingEngineSuite) TestTaskQueueManager_CyclingBehavior() {
config := s.newConfig()
dbq := newUnversionedRootQueueKey(uuid.NewString(), "makeToast", enumspb.TASK_QUEUE_TYPE_ACTIVITY)
for range 4 {
prevGetTasksCount := s.taskManager.getGetTasksCount(dbq)
mgr := s.newPartitionManager(dbq.partition, config)
mgr.Start()
// tlMgr.taskWriter startup is async so give it time to complete
time.Sleep(100 * time.Millisecond)
mgr.Stop(unloadCauseUnspecified)
s.LessOrEqual(s.taskManager.getGetTasksCount(dbq)-prevGetTasksCount, 1)
}
}
// TODO: should be moved to backlog_manager_test
func (s *matchingEngineSuite) TestTaskExpiryAndCompletion() {
if s.newMatcher {
s.T().Skip("not supported by new matcher")
}
runID := uuid.NewString()
workflowID := uuid.NewString()
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
namespaceID := uuid.NewString()
tl := "task-expiry-completion-tl0"
dbq := newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
taskQueue := &taskqueuepb.TaskQueue{
Name: tl,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
const taskCount = 20 // must be multiple of 4
const rangeSize = 10
s.matchingEngine.config.RangeSize = rangeSize
s.matchingEngine.config.MaxTaskDeleteBatchSize = dynamicconfig.GetIntPropertyFnFilteredByTaskQueue(2)
testCases := []struct {
maxTimeBtwnDeletes time.Duration
}{
{time.Minute}, // test taskGC deleting due to size threshold
{time.Nanosecond}, // test taskGC deleting due to time condition
}
for _, tc := range testCases {
for i := range int64(taskCount) {
scheduledEventID := i * 3
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: scheduledEventID,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
switch i % 4 {
case 0:
// simulates creating a task whose scheduledToStartTimeout is already expired
addRequest.ScheduleToStartTimeout = timestamp.DurationFromSeconds(-5)
case 2:
// simulates creating a task which will time out in the buffer
addRequest.ScheduleToStartTimeout = durationpb.New(250 * time.Millisecond)
}
_, _, err := s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
s.NoError(err)
}
tlMgr := s.getPhysicalTaskQueueManagerImplFromKey(dbq)
s.EqualValues(taskCount, s.taskManager.getTaskCount(dbq))
blm := tlMgr.backlogMgr.(*backlogManagerImpl)
// wait until all tasks are loaded by into in-memory buffers by task queue manager
// the buffer size should be one less than expected because dispatcher will dequeue the head
// 1/4 should be thrown out because they are expired before they hit the buffer
s.Eventually(func() bool { return len(blm.taskReader.taskBuffer) >= (3*taskCount/4 - 1) },
time.Second, 5*time.Millisecond)
// ensure the 1/4 of tasks with small ScheduleToStartTimeout will be expired when they come out of the buffer
time.Sleep(300 * time.Millisecond)
maxTimeBetweenTaskDeletes = tc.maxTimeBtwnDeletes
s.setupRecordActivityTaskStartedMock(tl)
pollReq := &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{TaskQueue: taskQueue, Identity: "test"},
}
remaining := taskCount
for range 2 {
// verify that (1) expired tasks are not returned in poll result (2) taskCleaner deletes tasks correctly
for range int64(taskCount / 4) {
result, err := s.matchingEngine.PollActivityTaskQueue(context.Background(), pollReq, metrics.NoopMetricsHandler)
s.NoError(err)
s.NotNil(result)
s.NotEqual(result, emptyPollActivityTaskQueueResponse)
}
remaining -= taskCount / 2
// since every other task is expired, we expect half the tasks to be deleted
// after poll consumed 1/4th of what is available.
// however, the gc is best-effort and might not run exactly when we want it to.
// various thread interleavings between the two task reader threads and this one
// might leave the gc behind by up to 3 tasks, or ahead by up to 1.
delta := remaining - s.taskManager.getTaskCount(dbq)
s.Truef(-3 <= delta && delta <= 1, "remaining %d, getTaskCount %d", remaining, s.taskManager.getTaskCount(dbq))
}
// ensure full gc for the next case (twice in case one doesn't get the gc lock)
blm.taskGC.RunNow(blm.taskAckManager.getAckLevel())
blm.taskGC.RunNow(blm.taskAckManager.getAckLevel())
}
}
func (s *matchingEngineSuite) TestGetVersioningData() {
namespaceID := namespace.ID(uuid.NewString())
tq := "tupac"
// Ensure we can fetch without first needing to set anything
res, err := s.matchingEngine.GetWorkerBuildIdCompatibility(context.Background(), &matchingservice.GetWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID.String(),
Request: &workflowservice.GetWorkerBuildIdCompatibilityRequest{
Namespace: namespaceID.String(),
TaskQueue: tq,
MaxSets: 0,
},
})
s.NoError(err)
s.NotNil(res)
// Set a long list of versions
for i := range 10 {
id := fmt.Sprintf("%d", i)
res, err := s.matchingEngine.UpdateWorkerBuildIdCompatibility(context.Background(), &matchingservice.UpdateWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
Operation: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest_{
ApplyPublicRequest: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest{
Request: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{
Namespace: namespaceID.String(),
TaskQueue: tq,
Operation: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewBuildIdInNewDefaultSet{
AddNewBuildIdInNewDefaultSet: id,
},
},
},
},
})
s.NoError(err)
s.NotNil(res)
}
// Make a long compat-versions chain
for i := range 80 {
id := fmt.Sprintf("9.%d", i)
prevCompat := fmt.Sprintf("9.%d", i-1)
if i == 0 {
prevCompat = "9"
}
res, err := s.matchingEngine.UpdateWorkerBuildIdCompatibility(context.Background(), &matchingservice.UpdateWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
Operation: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest_{
ApplyPublicRequest: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest{
Request: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{
Namespace: namespaceID.String(),
TaskQueue: tq,
Operation: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewCompatibleBuildId{
AddNewCompatibleBuildId: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewCompatibleVersion{
NewBuildId: id,
ExistingCompatibleBuildId: prevCompat,
MakeSetDefault: false,
},
},
},
},
},
})
s.NoError(err)
s.NotNil(res)
}
// Ensure they all exist
res, err = s.matchingEngine.GetWorkerBuildIdCompatibility(context.Background(), &matchingservice.GetWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID.String(),
Request: &workflowservice.GetWorkerBuildIdCompatibilityRequest{
Namespace: namespaceID.String(),
TaskQueue: tq,
MaxSets: 0,
},
})
s.NoError(err)
majorSets := res.GetResponse().GetMajorVersionSets()
curDefault := majorSets[len(majorSets)-1]
s.NotNil(curDefault)
s.Equal("9", curDefault.GetBuildIds()[0])
lastNode := curDefault.GetBuildIds()[len(curDefault.GetBuildIds())-1]
s.Equal("9.79", lastNode)
s.Equal("0", majorSets[0].GetBuildIds()[0])
// Ensure depth limiting works
res, err = s.matchingEngine.GetWorkerBuildIdCompatibility(context.Background(), &matchingservice.GetWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID.String(),
Request: &workflowservice.GetWorkerBuildIdCompatibilityRequest{
Namespace: namespaceID.String(),
TaskQueue: tq,
MaxSets: 1,
},
})
s.NoError(err)
majorSets = res.GetResponse().GetMajorVersionSets()
curDefault = majorSets[len(majorSets)-1]
s.Equal("9", curDefault.GetBuildIds()[0])
lastNode = curDefault.GetBuildIds()[len(curDefault.GetBuildIds())-1]
s.Equal("9.79", lastNode)
s.Equal(1, len(majorSets))
res, err = s.matchingEngine.GetWorkerBuildIdCompatibility(context.Background(), &matchingservice.GetWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID.String(),
Request: &workflowservice.GetWorkerBuildIdCompatibilityRequest{
Namespace: namespaceID.String(),
TaskQueue: tq,
MaxSets: 5,
},
})
s.NoError(err)
majorSets = res.GetResponse().GetMajorVersionSets()
s.Equal("5", majorSets[0].GetBuildIds()[0])
}
func (s *matchingEngineSuite) TestGetTaskQueueUserData_NoData() {
namespaceID := namespace.ID(uuid.NewString())
tq := "tupac"
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
s.Nil(res.UserData.GetData())
}
func (s *matchingEngineSuite) TestGetTaskQueueUserData_ReturnsData() {
namespaceID := namespace.ID(uuid.NewString())
tq := "tupac"
userData := &persistencespb.VersionedTaskQueueUserData{
Version: 1,
Data: &persistencespb.TaskQueueUserData{Clock: &clockspb.HybridLogicalClock{WallClock: 123456}},
}
s.NoError(s.classicTaskManager.UpdateTaskQueueUserData(context.Background(),
&persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceID.String(),
Updates: map[string]*persistence.SingleTaskQueueUserDataUpdate{
tq: &persistence.SingleTaskQueueUserDataUpdate{
UserData: userData,
},
},
}))
userData.Version++
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
protoassert.ProtoEqual(s.T(), res.UserData, userData)
}
func (s *matchingEngineSuite) TestGetTaskQueueUserData_ReturnsEmpty() {
namespaceID := namespace.ID(uuid.NewString())
tq := "tupac"
userData := &persistencespb.VersionedTaskQueueUserData{
Version: 1,
Data: &persistencespb.TaskQueueUserData{Clock: &clockspb.HybridLogicalClock{WallClock: 123456}},
}
s.NoError(s.classicTaskManager.UpdateTaskQueueUserData(context.Background(),
&persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceID.String(),
Updates: map[string]*persistence.SingleTaskQueueUserDataUpdate{
tq: &persistence.SingleTaskQueueUserDataUpdate{
UserData: userData,
},
},
}))
userData.Version++
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: userData.Version,
})
s.NoError(err)
s.Nil(res.UserData.GetData())
}
func (s *matchingEngineSuite) TestGetTaskQueueUserData_LongPoll_Expires() {
namespaceID := namespace.ID(uuid.NewString())
tq := "tupac"
userData := &persistencespb.VersionedTaskQueueUserData{
Version: 1,
Data: &persistencespb.TaskQueueUserData{Clock: &clockspb.HybridLogicalClock{WallClock: 123456}},
}
s.NoError(s.classicTaskManager.UpdateTaskQueueUserData(context.Background(),
&persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceID.String(),
Updates: map[string]*persistence.SingleTaskQueueUserDataUpdate{
tq: &persistence.SingleTaskQueueUserDataUpdate{
UserData: userData,
},
},
}))
userData.Version++
// GetTaskQueueUserData will try to return 5s with a min of 1s before the deadline, so this will block 1s
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
start := time.Now()
res, err := s.matchingEngine.GetTaskQueueUserData(ctx, &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: userData.Version,
WaitNewData: true,
})
s.NoError(err)
s.Nil(res.UserData.GetData())
elapsed := time.Since(start)
s.Greater(elapsed, 900*time.Millisecond)
}
func (s *matchingEngineSuite) TestGetTaskQueueUserData_LongPoll_WakesUp_FromNothing() {
namespaceID := namespace.ID(uuid.NewString())
tq := "tupac"
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
go func() {
time.Sleep(200 * time.Millisecond)
_, err := s.matchingEngine.UpdateWorkerBuildIdCompatibility(context.Background(), &matchingservice.UpdateWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
Operation: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest_{
ApplyPublicRequest: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest{
Request: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{
Namespace: namespaceID.String(),
TaskQueue: tq,
Operation: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewBuildIdInNewDefaultSet{
AddNewBuildIdInNewDefaultSet: "v1",
},
},
},
},
})
s.NoError(err)
}()
res, err := s.matchingEngine.GetTaskQueueUserData(ctx, &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0, // must be zero to start
WaitNewData: true,
})
s.NoError(err)
s.NotNil(res.UserData.Data.VersioningData)
}
func (s *matchingEngineSuite) TestGetTaskQueueUserData_LongPoll_WakesUp_From2to3() {
namespaceID := namespace.ID(uuid.NewString())
tq := "tupac"
userData := &persistencespb.VersionedTaskQueueUserData{
Version: 1,
Data: &persistencespb.TaskQueueUserData{Clock: &clockspb.HybridLogicalClock{WallClock: 123456}},
}
s.NoError(s.classicTaskManager.UpdateTaskQueueUserData(context.Background(),
&persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceID.String(),
Updates: map[string]*persistence.SingleTaskQueueUserDataUpdate{
tq: &persistence.SingleTaskQueueUserDataUpdate{
UserData: userData,
},
},
}))
userData.Version++
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
go func() {
time.Sleep(200 * time.Millisecond)
_, err := s.matchingEngine.UpdateWorkerBuildIdCompatibility(context.Background(), &matchingservice.UpdateWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
Operation: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest_{
ApplyPublicRequest: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest{
Request: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{
Namespace: namespaceID.String(),
TaskQueue: tq,
Operation: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewBuildIdInNewDefaultSet{
AddNewBuildIdInNewDefaultSet: "v1",
},
},
},
},
})
s.NoError(err)
}()
res, err := s.matchingEngine.GetTaskQueueUserData(ctx, &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: userData.Version,
WaitNewData: true,
})
s.NoError(err)
s.True(hlc.Greater(res.UserData.Data.Clock, userData.Data.Clock))
s.NotNil(res.UserData.Data.VersioningData)
}
func (s *matchingEngineSuite) TestGetTaskQueueUserData_LongPoll_Closes() {
namespaceID := namespace.ID(uuid.NewString())
tq := "tupac"
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
go func() {
time.Sleep(200 * time.Millisecond)
_, _ = s.matchingEngine.ForceUnloadTaskQueuePartition(context.Background(), &matchingservice.ForceUnloadTaskQueuePartitionRequest{
NamespaceId: namespaceID.String(),
TaskQueuePartition: &taskqueuespb.TaskQueuePartition{
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
},
})
}()
res, err := s.matchingEngine.GetTaskQueueUserData(ctx, &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
WaitNewData: true,
})
s.NoError(err)
s.Nil(res.UserData)
}
func (s *matchingEngineSuite) TestUpdateUserData_FailsOnKnownVersionMismatch() {
namespaceID := namespace.ID(uuid.NewString())
tq := "tupac"
userData := &persistencespb.VersionedTaskQueueUserData{
Version: 1,
Data: &persistencespb.TaskQueueUserData{Clock: &clockspb.HybridLogicalClock{WallClock: 123456}},
}
err := s.classicTaskManager.UpdateTaskQueueUserData(context.Background(),
&persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceID.String(),
Updates: map[string]*persistence.SingleTaskQueueUserDataUpdate{
tq: &persistence.SingleTaskQueueUserDataUpdate{
UserData: userData,
},
},
})
s.NoError(err)
_, err = s.matchingEngine.UpdateWorkerBuildIdCompatibility(context.Background(), &matchingservice.UpdateWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID.String(),
TaskQueue: tq,
Operation: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_RemoveBuildIds_{
RemoveBuildIds: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_RemoveBuildIds{
KnownUserDataVersion: 1,
},
},
})
var failedPreconditionError *serviceerror.FailedPrecondition
s.ErrorAs(err, &failedPreconditionError)
}
func (s *matchingEngineSuite) TestUnknownBuildId_Match() {
namespaceID := uuid.NewString()
tq := "makeToast"
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
s.mockMatchingClient.EXPECT().UpdateWorkerBuildIdCompatibility(gomock.Any(), &matchingservice.UpdateWorkerBuildIdCompatibilityRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
Operation: &matchingservice.UpdateWorkerBuildIdCompatibilityRequest_PersistUnknownBuildId{
PersistUnknownBuildId: "unknown",
},
}).Return(&matchingservice.UpdateWorkerBuildIdCompatibilityResponse{}, nil).AnyTimes() // might get called again on dispatch from spooled
var wg sync.WaitGroup
wg.Add(2)
go func() {
_, _, err := s.matchingEngine.AddWorkflowTask(ctx, &matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{RunId: "run", WorkflowId: "wf"},
ScheduledEventId: 123,
TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
// do not set ForwardedSource, allow to go to db
VersionDirective: worker_versioning.MakeBuildIdDirective("unknown"),
})
s.NoError(err)
wg.Done()
}()
go func() {
prtn := newRootPartition(namespaceID, tq, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
task, _, err := s.matchingEngine.pollTask(ctx, prtn, &pollMetadata{
workerVersionCapabilities: &commonpb.WorkerVersionCapabilities{
BuildId: "unknown",
UseVersioning: true,
},
})
s.NoError(err)
s.Equal("wf", task.event.Data.WorkflowId)
s.Equal(int64(123), task.event.Data.ScheduledEventId)
task.finish(nil, true)
wg.Done()
}()
wg.Wait()
}
func (s *matchingEngineSuite) TestDemotedMatch() {
namespaceID := uuid.NewString()
tq := "makeToast"
build0 := "build0"
build1 := "build1"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// add build0 as the overall default
clk := hlc.Zero(1)
userData := &persistencespb.TaskQueueUserData{
Clock: clk,
VersioningData: &persistencespb.VersioningData{
VersionSets: []*persistencespb.CompatibleVersionSet{
{
SetIds: []string{hashBuildId(build0)},
BuildIds: []*persistencespb.BuildId{
mkBuildId(build0, clk),
},
BecameDefaultTimestamp: clk,
},
},
},
}
err := s.classicTaskManager.UpdateTaskQueueUserData(ctx, &persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceID,
Updates: map[string]*persistence.SingleTaskQueueUserDataUpdate{
tq: &persistence.SingleTaskQueueUserDataUpdate{
UserData: &persistencespb.VersionedTaskQueueUserData{
Data: userData,
Version: 34,
},
},
},
})
s.Assert().NoError(err)
// add a task for build0, will get spooled in its set
_, _, err = s.matchingEngine.AddWorkflowTask(ctx, &matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{RunId: "run", WorkflowId: "wf"},
ScheduledEventId: 123,
TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
VersionDirective: worker_versioning.MakeBuildIdDirective(build0),
})
s.NoError(err)
// allow taskReader to finish starting dispatch loop so that we can unload tqms cleanly
time.Sleep(10 * time.Millisecond)
// unload base and versioned tqMgr. note: unload the partition manager unloads both
prtn := newRootPartition(namespaceID, tq, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
baseTqm, _, err := s.matchingEngine.getTaskQueuePartitionManager(ctx, prtn, false, loadCauseUnspecified)
s.NoError(err)
s.NotNil(baseTqm)
s.matchingEngine.unloadTaskQueuePartition(baseTqm, unloadCauseUnspecified)
// wait for taskReader goroutines to exit
baseTqm.(*taskQueuePartitionManagerImpl).userDataManager.(*userDataManagerImpl).goroGroup.Wait()
// both are now unloaded. change versioning data to merge unknown into another set.
userData = &persistencespb.TaskQueueUserData{
Clock: clk,
VersioningData: &persistencespb.VersioningData{
VersionSets: []*persistencespb.CompatibleVersionSet{
{
// make build0 set the demoted one to test demoted set loading.
// it works the other way too but doesn't test anything new.
SetIds: []string{hashBuildId(build1), hashBuildId(build0)},
BuildIds: []*persistencespb.BuildId{
mkBuildId(build0, clk),
mkBuildId(build1, clk),
},
BecameDefaultTimestamp: clk,
},
},
},
}
err = s.classicTaskManager.UpdateTaskQueueUserData(ctx, &persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceID,
Updates: map[string]*persistence.SingleTaskQueueUserDataUpdate{
tq: &persistence.SingleTaskQueueUserDataUpdate{
UserData: &persistencespb.VersionedTaskQueueUserData{
Data: userData,
Version: 34,
},
},
},
})
s.NoError(err)
// now poll for the task
task, _, err := s.matchingEngine.pollTask(ctx, prtn, &pollMetadata{
workerVersionCapabilities: &commonpb.WorkerVersionCapabilities{
BuildId: build1,
UseVersioning: true,
},
})
s.Require().NoError(err)
s.Equal("wf", task.event.Data.WorkflowId)
s.Equal(int64(123), task.event.Data.ScheduledEventId)
task.finish(nil, true)
}
type mockRoutingMatchingClient struct {
*matchingservicemock.MockMatchingServiceClient
sr membership.ServiceResolver
}
func (m mockRoutingMatchingClient) Route(p tqid.Partition) (string, error) {
key, n := p.RoutingKey(0)
hosts := m.sr.LookupN(key, n+1)
if len(hosts) == 0 {
return "", membership.ErrInsufficientHosts
}
return hosts[n%len(hosts)].GetAddress(), nil
}
func (s *matchingEngineSuite) TestUnloadOnMembershipChange() {
// need to create a new engine for this test to customize mockServiceResolver
s.mockServiceResolver = membership.NewMockServiceResolver(s.controller)
s.mockServiceResolver.EXPECT().AddListener(gomock.Any(), gomock.Any()).AnyTimes()
s.mockServiceResolver.EXPECT().RemoveListener(gomock.Any()).AnyTimes()
// add Route to MockMatchingServiceClient
routingClient := mockRoutingMatchingClient{
MockMatchingServiceClient: s.mockMatchingClient,
sr: s.mockServiceResolver,
}
self := s.mockHostInfoProvider.HostInfo()
other := membership.NewHostInfoFromAddress("other")
config := s.newConfig()
config.MembershipUnloadDelay = dynamicconfig.GetDurationPropertyFn(10 * time.Millisecond)
e := newMatchingEngine(config, s.classicTaskManager, s.fairTaskManager, s.mockHistoryClient,
s.logger, s.mockNamespaceCache, routingClient, s.mockVisibilityManager,
s.mockHostInfoProvider, s.mockServiceResolver, s.mockNexusEndpointManager)
e.Start()
defer e.Stop()
p1, err := tqid.NormalPartitionFromRpcName("makeToast", uuid.NewString(), enumspb.TASK_QUEUE_TYPE_WORKFLOW)
s.NoError(err)
p2, err := tqid.NormalPartitionFromRpcName("makeToast", uuid.NewString(), enumspb.TASK_QUEUE_TYPE_ACTIVITY)
s.NoError(err)
_, _, err = e.getTaskQueuePartitionManager(context.Background(), p1, true, loadCauseUnspecified)
s.NoError(err)
_, _, err = e.getTaskQueuePartitionManager(context.Background(), p2, true, loadCauseUnspecified)
s.NoError(err)
s.Equal(2, len(e.getTaskQueuePartitions(1000)))
s.mockServiceResolver.EXPECT().Lookup(nexusEndpointsTablePartitionRoutingKey).Return(self, nil).AnyTimes()
// signal membership changed and give time for loop to wake up
p1key, p1n := p1.RoutingKey(0)
p2key, p2n := p2.RoutingKey(0)
s.mockServiceResolver.EXPECT().LookupN(p1key, p1n+1).Return([]membership.HostInfo{self})
s.mockServiceResolver.EXPECT().LookupN(p2key, p2n+1).Return([]membership.HostInfo{self})
e.membershipChangedCh <- nil
time.Sleep(50 * time.Millisecond)
s.Equal(2, len(e.getTaskQueuePartitions(1000)), "nothing should be unloaded yet")
// signal again but p2 doesn't belong to us anymore
s.mockServiceResolver.EXPECT().LookupN(p1key, p1n+1).Return([]membership.HostInfo{self})
s.mockServiceResolver.EXPECT().LookupN(p2key, p2n+1).Return([]membership.HostInfo{other}).Times(2)
e.membershipChangedCh <- nil
s.Eventually(func() bool {
return len(e.getTaskQueuePartitions(1000)) == 1
}, 100*time.Millisecond, 10*time.Millisecond, "p2 should have been unloaded")
isLoaded := func(p tqid.Partition) bool {
tqm, _, err := e.getTaskQueuePartitionManager(context.Background(), p, false, loadCauseUnspecified)
s.NoError(err)
return tqm != nil
}
s.True(isLoaded(p1))
s.False(isLoaded(p2))
}
func (s *matchingEngineSuite) TaskQueueMetricValidator(capture *metricstest.Capture, familyCounterLength int, familyCounter float64, queueCounterLength int, queueCounter float64, queuePartitionCounterLength int, queuePartitionCounter float64) {
// checks the metrics according to the values passed in the parameters
snapshot := capture.Snapshot()
familyCounterRecordings := snapshot[metrics.LoadedTaskQueueFamilyGauge.Name()]
s.Len(familyCounterRecordings, familyCounterLength)
s.Equal(familyCounter, familyCounterRecordings[familyCounterLength-1].Value.(float64))
queueCounterRecordings := snapshot[metrics.LoadedTaskQueueGauge.Name()]
s.Len(queueCounterRecordings, queueCounterLength)
s.Equal(queueCounter, queueCounterRecordings[queueCounterLength-1].Value.(float64))
queuePartitionCounterRecordings := snapshot[metrics.LoadedTaskQueuePartitionGauge.Name()]
s.Len(queuePartitionCounterRecordings, queuePartitionCounterLength)
s.Equal(queuePartitionCounter, queuePartitionCounterRecordings[queuePartitionCounterLength-1].Value.(float64))
}
func (s *matchingEngineSuite) PhysicalQueueMetricValidator(capture *metricstest.Capture, physicalTaskQueueLength int, physicalTaskQueueCounter float64) {
// checks the metrics according to the values passed in the parameters
s.Eventually(func() bool {
snapshot := capture.Snapshot()
physicalTaskQueueRecordings := snapshot[metrics.LoadedPhysicalTaskQueueGauge.Name()]
return len(physicalTaskQueueRecordings) == physicalTaskQueueLength &&
physicalTaskQueueCounter == physicalTaskQueueRecordings[physicalTaskQueueLength-1].Value.(float64)
}, 5*time.Second, 100*time.Millisecond)
}
func (s *matchingEngineSuite) TestUpdateTaskQueuePartitionGauge_RootPartitionWorkflowType() {
// for getting snapshots of metrics
s.matchingEngine.metricsHandler = metricstest.NewCaptureHandler()
captureHandler, ok := s.matchingEngine.metricsHandler.(*metricstest.CaptureHandler)
s.True(ok)
capture := captureHandler.StartCapture()
rootPrtn := newRootPartition(uuid.NewString(), "MetricTester", enumspb.TASK_QUEUE_TYPE_WORKFLOW)
_, _, err := s.matchingEngine.getTaskQueuePartitionManager(context.Background(), rootPrtn, true, loadCauseUnspecified)
s.Require().NoError(err)
s.TaskQueueMetricValidator(capture, 1, 1, 1, 1, 1, 1)
// Calling the update gauge function should increase each of the metrics to 2
// since we are dealing with a root partition
s.matchingEngine.updateTaskQueuePartitionGauge(s.ns, rootPrtn, 1)
s.TaskQueueMetricValidator(capture, 2, 2, 2, 2, 2, 2)
}
func (s *matchingEngineSuite) TestUpdateTaskQueuePartitionGauge_RootPartitionActivityType() {
// for getting snapshots of metrics
s.matchingEngine.metricsHandler = metricstest.NewCaptureHandler()
captureHandler, ok := s.matchingEngine.metricsHandler.(*metricstest.CaptureHandler)
s.True(ok)
capture := captureHandler.StartCapture()
rootPrtn := newRootPartition(uuid.NewString(), "MetricTester", enumspb.TASK_QUEUE_TYPE_ACTIVITY)
_, _, err := s.matchingEngine.getTaskQueuePartitionManager(context.Background(), rootPrtn, true, loadCauseUnspecified)
s.Require().NoError(err)
// Creation of a new root partition, having an activity task queue, should not have
// increased FamilyCounter. Other metrics should be 1.
s.TaskQueueMetricValidator(capture, 1, 0, 1, 1, 1, 1)
// Calling the update gauge function should increase each of the metrics to 2
// since we are dealing with a root partition
s.matchingEngine.updateTaskQueuePartitionGauge(s.ns, rootPrtn, 1)
s.TaskQueueMetricValidator(capture, 2, 0, 2, 2, 2, 2)
}
func (s *matchingEngineSuite) TestUpdateTaskQueuePartitionGauge_NonRootPartition() {
s.matchingEngine.metricsHandler = metricstest.NewCaptureHandler()
captureHandler, ok := s.matchingEngine.metricsHandler.(*metricstest.CaptureHandler)
s.True(ok)
capture := captureHandler.StartCapture()
nonRootPrtn := newTestTaskQueue(uuid.NewString(), "MetricTester", enumspb.TASK_QUEUE_TYPE_WORKFLOW).NormalPartition(31)
_, _, err := s.matchingEngine.getTaskQueuePartitionManager(context.Background(), nonRootPrtn, true, loadCauseUnspecified)
s.Require().NoError(err)
// Creation of a non-root partition should only increase the Queue Partition counter
s.TaskQueueMetricValidator(capture, 1, 0, 1, 0, 1, 1)
// Calling the update gauge function should increase each of the metrics to 2
// since we are dealing with a root partition
s.matchingEngine.updateTaskQueuePartitionGauge(s.ns, nonRootPrtn, 1)
s.TaskQueueMetricValidator(capture, 2, 0, 2, 0, 2, 2)
}
func (s *matchingEngineSuite) TestUpdatePhysicalTaskQueueGauge_UnVersioned() {
s.matchingEngine.metricsHandler = metricstest.NewCaptureHandler()
captureHandler, ok := s.matchingEngine.metricsHandler.(*metricstest.CaptureHandler)
s.True(ok)
capture := captureHandler.StartCapture()
prtn := newRootPartition(
uuid.NewString(),
"MetricTester",
enumspb.TASK_QUEUE_TYPE_ACTIVITY)
tqm, _, err := s.matchingEngine.getTaskQueuePartitionManager(context.Background(), prtn, true, loadCauseUnspecified)
s.Require().NoError(err)
// Creating a TaskQueuePartitionManager results in creating a PhysicalTaskQueueManager which should increase
// the size of the map to 1 and it's counter to 1.
s.PhysicalQueueMetricValidator(capture, 1, 1)
tlmImpl := s.getPhysicalTaskQueueManagerImpl(tqm)
s.matchingEngine.updatePhysicalTaskQueueGauge(s.ns, prtn, tlmImpl.queue.version, 1)
s.PhysicalQueueMetricValidator(capture, 2, 2)
// Validating if versioned has been set right for the respective parameters
physicalTaskQueueParameters := taskQueueCounterKey{
namespaceID: prtn.NamespaceId(),
taskType: prtn.TaskType(),
partitionType: prtn.Kind(),
versioned: "unversioned",
}
assert.Equal(s.T(), s.matchingEngine.gaugeMetrics.loadedPhysicalTaskQueueCount[physicalTaskQueueParameters], 2)
}
func (s *matchingEngineSuite) TestUpdatePhysicalTaskQueueGauge_VersionSet() {
s.matchingEngine.metricsHandler = metricstest.NewCaptureHandler()
captureHandler, ok := s.matchingEngine.metricsHandler.(*metricstest.CaptureHandler)
s.True(ok)
capture := captureHandler.StartCapture()
namespaceID := uuid.NewString()
versionSet := uuid.NewString()
tl := "MetricTester"
rootPrtn := newTestTaskQueue(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY).RootPartition()
dbq := VersionSetQueueKey(rootPrtn, versionSet)
tqm, _, err := s.matchingEngine.getTaskQueuePartitionManager(context.Background(), dbq.Partition(), true, loadCauseUnspecified)
s.Require().NoError(err)
// Creating a TaskQueuePartitionManager results in creating a PhysicalTaskQueueManager which should increase
// the size of the map to 1 and it's counter to 1.
s.PhysicalQueueMetricValidator(capture, 1, 1)
vqtpm, err := tqm.(*taskQueuePartitionManagerImpl).getVersionedQueueNoWait(
versionSet,
"",
nil,
true,
)
s.Require().NoError(err)
// Creating a VersionedQueue results in increasing the size of the map to 2, due to 2 entries now,
// with it's counter to 1.
s.PhysicalQueueMetricValidator(capture, 2, 1)
s.matchingEngine.updatePhysicalTaskQueueGauge(s.ns, rootPrtn, vqtpm.(*physicalTaskQueueManagerImpl).queue.version, 1)
s.PhysicalQueueMetricValidator(capture, 3, 2)
// Validating if versioned has been set right for the specific parameters
physicalTaskQueueParameters := taskQueueCounterKey{
namespaceID: dbq.Partition().NamespaceId(),
taskType: dbq.Partition().TaskType(),
partitionType: dbq.Partition().Kind(),
versioned: "versionSet",
}
assert.Equal(s.T(), s.matchingEngine.gaugeMetrics.loadedPhysicalTaskQueueCount[physicalTaskQueueParameters], 2)
}
func (s *matchingEngineSuite) TestUpdatePhysicalTaskQueueGauge_BuildID() {
s.matchingEngine.metricsHandler = metricstest.NewCaptureHandler()
captureHandler, ok := s.matchingEngine.metricsHandler.(*metricstest.CaptureHandler)
s.True(ok)
capture := captureHandler.StartCapture()
namespaceID := uuid.NewString()
buildID := uuid.NewString()
rootPrtn := newTestTaskQueue(namespaceID, "MetricTester", enumspb.TASK_QUEUE_TYPE_ACTIVITY).RootPartition()
dbq := BuildIdQueueKey(rootPrtn, buildID)
tqm, _, err := s.matchingEngine.getTaskQueuePartitionManager(context.Background(), dbq.Partition(), true, loadCauseUnspecified)
s.Require().NoError(err)
// Creating a TaskQueuePartitionManager results in creating a PhysicalTaskQueueManager which should increase
// the size of the map to 1 and it's counter to 1.
s.PhysicalQueueMetricValidator(capture, 1, 1)
vqtpm, err := tqm.(*taskQueuePartitionManagerImpl).getVersionedQueueNoWait(
"",
buildID,
nil,
true,
)
s.Require().NoError(err)
// Creating a VersionedQueue results in increasing the size of the map to 2, due to 2 entries now,
// with it's counter to 1.
s.PhysicalQueueMetricValidator(capture, 2, 1)
s.matchingEngine.updatePhysicalTaskQueueGauge(s.ns, rootPrtn, vqtpm.(*physicalTaskQueueManagerImpl).queue.version, 1)
s.PhysicalQueueMetricValidator(capture, 3, 2)
// Validating if versioned has been set right for the specific parameters
physicalTaskQueueParameters := taskQueueCounterKey{
namespaceID: dbq.Partition().NamespaceId(),
taskType: dbq.Partition().TaskType(),
partitionType: dbq.Partition().Kind(),
versioned: "buildId",
}
assert.Equal(s.T(), s.matchingEngine.gaugeMetrics.loadedPhysicalTaskQueueCount[physicalTaskQueueParameters], 2)
}
// generateWorkflowExecution makes a sample workflowExecution and WorkflowType for the required tests
func (s *matchingEngineSuite) generateWorkflowExecution() (*commonpb.WorkflowType, *commonpb.WorkflowExecution) {
runID := uuid.NewString()
workflowID := "workflow1"
workflowType := &commonpb.WorkflowType{
Name: "workflow",
}
workflowExecution := &commonpb.WorkflowExecution{RunId: runID, WorkflowId: workflowID}
return workflowType, workflowExecution
}
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 ...any) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
return &historyservice.RecordWorkflowTaskStartedResponse{
PreviousStartedEventId: 1,
StartedEventId: 1,
ScheduledEventId: 1,
WorkflowType: workflowType,
Attempt: 1,
History: &historypb.History{Events: []*historypb.HistoryEvent{}},
NextPageToken: nil,
}, nil
}).AnyTimes()
}
func (s *matchingEngineSuite) createTQAndPTQForBacklogTests() (*taskqueuepb.TaskQueue, *PhysicalTaskQueueKey) {
s.matchingEngine.config.RangeSize = 10
tq := "approximateBacklogCounter"
ptq := newUnversionedRootQueueKey(namespaceID, tq, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
taskQueue := &taskqueuepb.TaskQueue{
Name: tq,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
return taskQueue, ptq
}
func (s *matchingEngineSuite) addWorkflowTask(
workflowExecution *commonpb.WorkflowExecution,
taskQueue *taskqueuepb.TaskQueue,
) {
s.EventuallyWithT(func(c *assert.CollectT) {
addRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID,
Execution: workflowExecution,
ScheduledEventId: 1,
TaskQueue: taskQueue,
ScheduleToStartTimeout: timestamp.DurationFromSeconds(100),
}
_, _, err := s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
require.NoError(c, err)
}, 10*time.Second, time.Millisecond, "failed to add workflow task")
}
func (s *matchingEngineSuite) createPollWorkflowTaskRequestAndPoll(taskQueue *taskqueuepb.TaskQueue) {
s.EventuallyWithT(func(c *assert.CollectT) {
result, err := s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: taskQueue,
Identity: "nobody",
},
}, metrics.NoopMetricsHandler)
require.NoError(c, err) // DB could have failed while fetching tasks; try again
require.NotEmpty(c, result.TaskToken)
require.NotZero(c, result.Attempt)
}, 3*time.Second, time.Millisecond, "failed to poll workflow task")
}
// addWorkflowTasks adds taskCount number of tasks sequentially
func (s *matchingEngineSuite) addWorkflowTasks(
taskCount int, taskQueue *taskqueuepb.TaskQueue, workflowExecution *commonpb.WorkflowExecution,
) {
for range taskCount {
s.addWorkflowTask(workflowExecution, taskQueue)
}
}
// addWorkflowTasksConcurrent adds taskCount number of tasks for each numWorker concurrently
func (s *matchingEngineSuite) addWorkflowTasksConcurrently(
wg *sync.WaitGroup, numWorkers int, taskCount int,
taskQueue *taskqueuepb.TaskQueue, workflowExecution *commonpb.WorkflowExecution,
) {
for range numWorkers {
wg.Go(func() {
for range taskCount {
s.addWorkflowTask(workflowExecution, taskQueue)
}
})
}
}
// pollWorkflowTasks polls tasks sequentially
func (s *matchingEngineSuite) pollWorkflowTasks(
workflowType *commonpb.WorkflowType, taskCount int,
ptq *PhysicalTaskQueueKey, taskQueue *taskqueuepb.TaskQueue,
) {
s.mockHistoryWhilePolling(workflowType)
tasksPolled := 0
for range taskCount {
s.createPollWorkflowTaskRequestAndPoll(taskQueue)
tasksPolled += 1
// relax ApproximateBacklogCount for fairness impl
if !s.fairness {
// PartitionManager could have been unloaded; fetch the latest copy
pgMgr := s.getPhysicalTaskQueueManagerImplFromKey(ptq)
s.LessOrEqual(int64(taskCount-tasksPolled), totalApproximateBacklogCount(pgMgr.backlogMgr))
}
}
}
// pollWorkflowTasksConcurrently polls tasks using numWorkers concurrently
func (s *matchingEngineSuite) pollWorkflowTasksConcurrently(
wg *sync.WaitGroup, workflowType *commonpb.WorkflowType, numPollers int, taskCount int,
ptq *PhysicalTaskQueueKey, taskQueue *taskqueuepb.TaskQueue,
) {
s.mockHistoryWhilePolling(workflowType)
for range numPollers {
wg.Go(func() {
for range taskCount {
s.createPollWorkflowTaskRequestAndPoll(taskQueue)
}
})
}
}
func (s *matchingEngineSuite) getTaskQueuePartitionManagerImpl(ptq *PhysicalTaskQueueKey) *taskQueuePartitionManagerImpl {
return s.matchingEngine.partitions[ptq.Partition().Key()].(*taskQueuePartitionManagerImpl)
}
// getPhysicalTaskQueueManagerImpl extracts the physicalTaskQueueManagerImpl for the given taskQueuePartitionManager
func (s *matchingEngineSuite) getPhysicalTaskQueueManagerImpl(mgr taskQueuePartitionManager) *physicalTaskQueueManagerImpl {
defaultQ, err := mgr.(*taskQueuePartitionManagerImpl).defaultQueueFuture.GetIfReady()
s.Require().NoError(err)
return defaultQ.(*physicalTaskQueueManagerImpl)
}
func (s *matchingEngineSuite) getPhysicalTaskQueueManagerImplFromKey(ptq *PhysicalTaskQueueKey) *physicalTaskQueueManagerImpl {
return s.getPhysicalTaskQueueManagerImpl(s.getTaskQueuePartitionManagerImpl(ptq))
}
func (s *matchingEngineSuite) addConsumeAllWorkflowTasksNonConcurrently(taskCount int) {
workflowType, workflowExecution := s.generateWorkflowExecution()
taskQueue, ptq := s.createTQAndPTQForBacklogTests()
s.addWorkflowTasks(taskCount, taskQueue, workflowExecution)
s.Equal(taskCount, s.taskManager.getCreateTaskCount(ptq))
s.Equal(taskCount, s.taskManager.getTaskCount(ptq))
pgMgr := s.getPhysicalTaskQueueManagerImplFromKey(ptq)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
err := pgMgr.WaitUntilInitialized(ctx)
s.Require().NoError(err)
cancel()
backlogCount := totalApproximateBacklogCount(pgMgr.backlogMgr)
if s.fairness {
// Relax this condition for fairBacklogManager: it can sometimes reset backlog count on
// read, making it more accurate in theory, but breaking this test's assumptions.
s.InDelta(taskCount, backlogCount, 2)
} else {
s.EqualValues(taskCount, backlogCount)
}
s.pollWorkflowTasks(workflowType, taskCount, ptq, taskQueue)
s.LessOrEqual(int64(0), totalApproximateBacklogCount(pgMgr.backlogMgr))
}
func (s *matchingEngineSuite) TestAddConsumeWorkflowTasksNoDBErrors() {
s.addConsumeAllWorkflowTasksNonConcurrently(200)
}
func (s *matchingEngineSuite) TestAddConsumeWorkflowTasksDBErrors() {
s.logger.Expect(testlogger.Error, "Persistent store operation failure")
s.logger.Expect(testlogger.Error, "unexpected error dispatching task")
s.taskManager.addFault("CreateTasks", "ConditionFailed", 0.1)
s.taskManager.addFault("GetTasks", "Unavailable", 0.1)
s.addConsumeAllWorkflowTasksNonConcurrently(200)
}
func (s *matchingEngineSuite) resetBacklogCounter(numWorkers int, taskCount int, rangeSize int) {
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(1 * time.Millisecond)
s.matchingEngine.config.UpdateAckInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(100 * time.Millisecond)
workflowType, workflowExecution := s.generateWorkflowExecution()
taskQueue, ptq := s.createTQAndPTQForBacklogTests()
s.matchingEngine.config.RangeSize = int64(rangeSize)
s.addWorkflowTasks(taskCount*numWorkers, taskQueue, workflowExecution)
// TaskID of the first task to be added
minTaskID, done := s.taskManager.minTaskID(ptq)
s.True(done)
partitionManager, _, err := s.matchingEngine.getTaskQueuePartitionManager(context.Background(), ptq.Partition(), false, loadCauseTask)
s.NoError(err)
pqMgr := s.getPhysicalTaskQueueManagerImplFromKey(ptq)
s.EqualValues(taskCount*numWorkers, s.taskManager.getTaskCount(ptq))
// Check the maxReadLevel with the value of task stored in db
maxTaskId, ok := s.taskManager.maxTaskID(ptq)
s.True(ok)
s.EqualValues(maxTaskId, pqMgr.backlogMgr.getDB().GetMaxReadLevel(0))
// validate the approximateBacklogCounter
s.EqualValues(taskCount*numWorkers, totalApproximateBacklogCount(pqMgr.backlogMgr))
// Unload the PQM
s.matchingEngine.unloadTaskQueuePartition(partitionManager, unloadCauseForce)
// Simulate a TTL'ed task in Cassandra by removing it from the DB
// Remove the task from testTaskManager but not from db/AckManager
// Stop the backlogManager so that we TTL and the taskReader does not catch this
request := &persistence.CompleteTasksLessThanRequest{
NamespaceID: namespaceID,
TaskQueueName: taskQueue.Name,
TaskType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
ExclusiveMaxTaskID: minTaskID + 1,
Limit: 100,
}
_, err = s.taskManager.CompleteTasksLessThan(context.Background(), request)
s.NoError(err)
s.EqualValues((taskCount*numWorkers)-1, s.taskManager.getTaskCount(ptq))
// Add pollers which shall also load the fresher version of tqMgr
s.pollWorkflowTasks(workflowType, (taskCount*numWorkers)-1, ptq, taskQueue)
// Update pgMgr to have the latest pgMgr
pqMgr = s.getPhysicalTaskQueueManagerImplFromKey(ptq)
// Overwrite the maxReadLevel since it could have increased if the previous taskWriter was
// stopped (which would not result in resetting).
pqMgr.backlogMgr.getDB().setMaxReadLevelForTesting(subqueueZero, maxTaskId)
s.EqualValues(0, s.taskManager.getTaskCount(ptq))
s.EventuallyWithT(func(collect *assert.CollectT) {
require.Equal(collect, int64(0), totalApproximateBacklogCount(pqMgr.backlogMgr))
}, 4*time.Second, 10*time.Millisecond, "backlog counter should have been reset")
}
// TestResettingBacklogCounter tests the scenario where approximateBacklogCounter over-counts and resets it accordingly
func (s *matchingEngineSuite) TestResetBacklogCounterNoDBErrors() {
if s.newMatcher {
s.T().Skip("not supported by new matcher; flaky")
}
s.resetBacklogCounter(2, 2, 2)
}
func (s *matchingEngineSuite) TestResetBacklogCounterDBErrors() {
if s.newMatcher {
s.T().Skip("test is flaky with new matcher")
}
s.logger.Expect(testlogger.Error, "Persistent store operation failure")
s.logger.Expect(testlogger.Error, "unexpected error dispatching task")
s.taskManager.addFault("CreateTasks", "ConditionFailed", 0.1)
s.taskManager.addFault("GetTasks", "Unavailable", 0.1)
s.resetBacklogCounter(2, 2, 2)
}
func (s *matchingEngineSuite) TestMoreTasksResetBacklogCounterNoDBErrors() {
if s.newMatcher {
s.T().Skip("test is flaky with new matcher")
}
s.resetBacklogCounter(10, 20, 2)
}
func (s *matchingEngineSuite) TestMoreTasksResetBacklogCounterDBErrors() {
if s.newMatcher {
s.T().Skip("test is flaky with new matcher")
}
s.logger.Expect(testlogger.Error, "Persistent store operation failure")
s.logger.Expect(testlogger.Error, "unexpected error dispatching task")
s.taskManager.addFault("CreateTasks", "ConditionFailed", 0.1)
s.taskManager.addFault("GetTasks", "Unavailable", 0.1)
s.resetBacklogCounter(10, 50, 5)
}
// Concurrent tests for testing approximateBacklogCounter
func (s *matchingEngineSuite) concurrentPublishAndConsumeValidateBacklogCounter(
numWorkers, tasksToAdd, tasksToPoll int,
) {
s.matchingEngine.config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(10 * time.Millisecond)
s.matchingEngine.config.UpdateAckInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(1 * time.Millisecond)
var wg sync.WaitGroup
workflowType, workflowExecution := s.generateWorkflowExecution()
taskQueue, ptq := s.createTQAndPTQForBacklogTests()
s.addWorkflowTasksConcurrently(&wg, numWorkers, tasksToAdd, taskQueue, workflowExecution)
s.pollWorkflowTasksConcurrently(&wg, workflowType, numWorkers, tasksToPoll, ptq, taskQueue)
wg.Wait()
ptqMgr := s.getPhysicalTaskQueueManagerImplFromKey(ptq)
dbTasks := int64(s.taskManager.getTaskCount(ptq))
backlogCount := totalApproximateBacklogCount(ptqMgr.backlogMgr)
if s.fairness {
// Relax this condition for fairBacklogManager: it can sometimes reset backlog count on
// read, making it more accurate in theory, but breaking this test's assumptions.
s.InDelta(dbTasks, backlogCount, 2)
} else {
s.LessOrEqual(dbTasks, backlogCount)
}
}
func (s *matchingEngineSuite) TestConcurrentAddWorkflowTasksNoDBErrors() {
s.concurrentPublishAndConsumeValidateBacklogCounter(150, 100, 0)
}
func (s *matchingEngineSuite) TestConcurrentAddWorkflowTasksDBErrors() {
s.T().Skip("Skipping this as the backlog counter could under-count. Fix requires making " +
"UpdateState an atomic operation.")
s.taskManager.addFault("CreateTasks", "ConditionFailed", 0.1)
s.taskManager.addFault("GetTasks", "Unavailable", 0.1)
s.concurrentPublishAndConsumeValidateBacklogCounter(150, 100, 0)
}
func (s *matchingEngineSuite) TestConcurrentAdd_PollWorkflowTasksNoDBErrors() {
if s.newMatcher {
s.T().Skip("test is flaky with new matcher")
}
s.concurrentPublishAndConsumeValidateBacklogCounter(20, 100, 100)
}
func (s *matchingEngineSuite) TestConcurrentAdd_PollWorkflowTasksDBErrors() {
s.T().Skip("Skipping this as the backlog counter could under-count. Fix requires making " +
"UpdateState an atomic operation.")
s.taskManager.addFault("CreateTasks", "ConditionFailed", 0.1)
s.taskManager.addFault("GetTasks", "Unavailable", 0.1)
s.concurrentPublishAndConsumeValidateBacklogCounter(20, 100, 100)
}
func (s *matchingEngineSuite) TestLesserNumberOfPollersThanTasksNoDBErrors() {
s.concurrentPublishAndConsumeValidateBacklogCounter(1, 500, 200)
}
func (s *matchingEngineSuite) TestLesserNumberOfPollersThanTasksDBErrors() {
s.logger.Expect(testlogger.Error, "Persistent store operation failure")
s.logger.Expect(testlogger.Error, "unexpected error dispatching task")
s.taskManager.addFault("CreateTasks", "ConditionFailed", 0.1)
s.taskManager.addFault("GetTasks", "Unavailable", 0.1)
s.concurrentPublishAndConsumeValidateBacklogCounter(1, 500, 200)
}
func (s *matchingEngineSuite) TestMultipleWorkersLesserNumberOfPollersThanTasksNoDBErrors() {
s.concurrentPublishAndConsumeValidateBacklogCounter(5, 500, 200)
}
func (s *matchingEngineSuite) TestMultipleWorkersLesserNumberOfPollersThanTasksDBErrors() {
s.T().Skip("Skipping this as the backlog counter could under-count. Fix requires making " +
"UpdateState an atomic operation.")
s.taskManager.addFault("CreateTasks", "ConditionFailed", 0.1)
s.taskManager.addFault("GetTasks", "Unavailable", 0.1)
s.concurrentPublishAndConsumeValidateBacklogCounter(5, 500, 200)
}
func (s *matchingEngineSuite) TestCheckNexusEndpointsOwnership() {
isOwner, _, err := s.matchingEngine.checkNexusEndpointsOwnership()
s.NoError(err)
s.True(isOwner)
s.hostInfoForResolver = membership.NewHostInfoFromAddress("other")
isOwner, _, err = s.matchingEngine.checkNexusEndpointsOwnership()
s.NoError(err)
s.False(isOwner)
}
func (s *matchingEngineSuite) TestNotifyNexusEndpointsOwnershipLost() {
ch := s.matchingEngine.nexusEndpointsOwnershipLostCh.Load().(chan struct{}) //nolint:revive // type is always chan struct{}
s.matchingEngine.notifyNexusEndpointsOwnershipChange()
select {
case <-ch:
s.Fail("expected nexusEndpointsOwnershipLost channel to not have been closed")
default:
}
s.hostInfoForResolver = membership.NewHostInfoFromAddress("other")
s.matchingEngine.notifyNexusEndpointsOwnershipChange()
<-ch
// If the channel is unblocked the test passed.
}
func (s *matchingEngineSuite) TestPollActivityTaskQueueWithRateLimiterError() {
mockRateLimiter := quotas.NewMockRequestRateLimiter(s.controller)
rateLimiterErr := serviceerror.NewResourceExhausted(enumspb.RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, "rate limit exceeded")
s.matchingEngine.rateLimiter = mockRateLimiter
namespaceID := uuid.NewString()
tl := "queue"
taskQueue := &taskqueuepb.TaskQueue{Name: "queue", Kind: enumspb.TASK_QUEUE_KIND_NORMAL}
addRequest := matchingservice.AddActivityTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{WorkflowId: "workflowID", RunId: uuid.NewString()},
ScheduledEventId: int64(5),
TaskQueue: taskQueue,
}
_, _, err := s.matchingEngine.AddActivityTask(context.Background(), &addRequest)
s.NoError(err)
s.Equal(1, s.taskManager.getTaskCount(newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_ACTIVITY)))
mockRateLimiter.EXPECT().
Wait(gomock.Any(), gomock.Any()).
Return(rateLimiterErr).Times(1)
s.matchingEngine.rateLimiter = mockRateLimiter
_, err = s.matchingEngine.PollActivityTaskQueue(context.Background(), &matchingservice.PollActivityTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollActivityTaskQueueRequest{
TaskQueue: taskQueue,
Identity: "identity",
},
}, metrics.NoopMetricsHandler)
s.ErrorIs(err, rateLimiterErr)
}
func (s *matchingEngineSuite) TestPollWorkflowTaskQueueWithRateLimiterError() {
mockRateLimiter := quotas.NewMockRequestRateLimiter(s.controller)
rateLimiterErr := serviceerror.NewResourceExhausted(enumspb.RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, "rate limit exceeded")
namespaceID := uuid.NewString()
tl := "queue"
taskQueue := &taskqueuepb.TaskQueue{Name: "queue", Kind: enumspb.TASK_QUEUE_KIND_NORMAL}
addRequest := matchingservice.AddWorkflowTaskRequest{
NamespaceId: namespaceID,
Execution: &commonpb.WorkflowExecution{WorkflowId: "workflowID", RunId: uuid.NewString()},
ScheduledEventId: int64(5),
TaskQueue: taskQueue,
}
_, _, err := s.matchingEngine.AddWorkflowTask(context.Background(), &addRequest)
s.NoError(err)
s.Equal(1, s.taskManager.getTaskCount(newUnversionedRootQueueKey(namespaceID, tl, enumspb.TASK_QUEUE_TYPE_WORKFLOW)))
mockRateLimiter.EXPECT().
Wait(gomock.Any(), gomock.Any()).
Return(rateLimiterErr).Times(1)
s.matchingEngine.rateLimiter = mockRateLimiter
_, err = s.matchingEngine.PollWorkflowTaskQueue(context.Background(), &matchingservice.PollWorkflowTaskQueueRequest{
NamespaceId: namespaceID,
PollRequest: &workflowservice.PollWorkflowTaskQueueRequest{
TaskQueue: taskQueue,
Identity: "identity",
},
}, metrics.NoopMetricsHandler)
s.ErrorIs(err, rateLimiterErr)
}
func (s *matchingEngineSuite) TestDispatchNexusTask_ValidateTimeoutBuffer() {
const ctxTimeout = 2 * time.Second
var defaultTimeoutBuffer = nexusoperations.MinDispatchTaskTimeout.Get(dynamicconfig.NewNoopCollection())("my-nsid")
type testCase struct {
name string
sleepTime time.Duration
assertion func(t *testing.T, response *matchingservice.DispatchNexusTaskResponse, err error)
}
testCases := []testCase{
{
name: "deadline_exceeded_immediately",
sleepTime: ctxTimeout - defaultTimeoutBuffer,
assertion: func(t *testing.T, response *matchingservice.DispatchNexusTaskResponse, err error) {
require.NoError(t, err)
require.NotNil(t, response.GetRequestTimeout())
},
},
{
name: "deadline_exceeded_awaiting_local_dispatch",
sleepTime: ctxTimeout - defaultTimeoutBuffer - time.Nanosecond,
assertion: func(t *testing.T, response *matchingservice.DispatchNexusTaskResponse, err error) {
require.NoError(t, err)
require.NotNil(t, response.GetRequestTimeout())
},
},
{
name: "deadline_not_exceeded_on_forwarding",
sleepTime: ctxTimeout - defaultTimeoutBuffer - time.Nanosecond,
assertion: func(t *testing.T, response *matchingservice.DispatchNexusTaskResponse, err error) {
require.NoError(t, err)
require.NotNil(t, response.GetResponse())
},
},
}
testFn := func(t *testing.T, tc testCase) {
synctest.Test(t, func(t *testing.T) {
nexusRequest := &matchingservice.DispatchNexusTaskRequest{
NamespaceId: "my-nsid",
TaskQueue: &taskqueuepb.TaskQueue{
Name: "my-tq",
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
Request: &nexuspb.Request{
Header: map[string]string{
"request-timeout": "2s",
},
},
}
mockPartitionManager := NewMocktaskQueuePartitionManager(s.controller)
mockPartitionManager.EXPECT().WaitUntilInitialized(gomock.Any()).Return(nil).AnyTimes()
mockPartitionManager.EXPECT().Stop(gomock.Any()).AnyTimes()
//nolint:forbidigo // We're safe to use the sleep here since we're using synctest to control the timing
mockPartitionManager.EXPECT().DispatchNexusTask(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, taskId string, request *matchingservice.DispatchNexusTaskRequest) (*matchingservice.DispatchNexusTaskResponse, error) {
time.Sleep(tc.sleepTime)
synctest.Wait()
if err := ctx.Err(); err != nil {
return nil, err
}
if tc.name == "deadline_exceeded_awaiting_local_dispatch" {
return nil, nil
}
return &matchingservice.DispatchNexusTaskResponse{Outcome: &matchingservice.DispatchNexusTaskResponse_Response{
Response: &nexuspb.Response{},
}}, nil
},
).Times(1)
partition := newRootPartition("my-nsid", "my-tq", enumspb.TASK_QUEUE_TYPE_NEXUS)
s.matchingEngine.partitions[partition.Key()] = mockPartitionManager
s.matchingEngine.nexusResults = collection.NewSyncMap[string, chan *nexusResult]()
ctx, cancel := context.WithTimeout(context.Background(), ctxTimeout)
defer cancel()
resp, err := s.matchingEngine.DispatchNexusTask(ctx, nexusRequest)
tc.assertion(t, resp, err)
})
}
for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
testFn(t, tc)
})
}
}
// Following are tests for SyncDeploymentUserData API when it uses the new deployment data format.
// TestSyncDeploymentUserData_NewDeploymentDataRemovesOldVersions verifies that when a new routing config is set for a deployment using the latest deployment data format,
// versions, under the same deployment, that had synced using the old deployment data format are removed.
func (s *matchingEngineSuite) TestSyncDeploymentUserData_NewDeploymentDataRemovesOldVersions() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
// Seed user data with old-format versions across two deployments: foo.A and bar.B
t1 := timestamp.TimePtr(time.Now().Add(-time.Hour))
userData := &persistencespb.VersionedTaskQueueUserData{
Version: 1,
Data: &persistencespb.TaskQueueUserData{
PerType: map[int32]*persistencespb.TaskQueueTypeUserData{
int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW): {
DeploymentData: &persistencespb.DeploymentData{
Versions: []*deploymentspb.DeploymentVersionData{
{Version: &deploymentspb.WorkerDeploymentVersion{DeploymentName: "foo", BuildId: "A"}, RoutingUpdateTime: t1, CurrentSinceTime: t1},
{Version: &deploymentspb.WorkerDeploymentVersion{DeploymentName: "bar", BuildId: "B"}, RoutingUpdateTime: t1, CurrentSinceTime: t1},
},
},
},
},
},
}
// Using the lower level UpdateTaskQueueUserData to set the user data for multiple versions at once.
s.NoError(s.classicTaskManager.UpdateTaskQueueUserData(context.Background(), &persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceID,
Updates: map[string]*persistence.SingleTaskQueueUserDataUpdate{
tq: {UserData: userData},
},
}))
userData.Version++
// Sync new-format routing config for deployment "foo" with a newer revision to trigger cleanup
rc := &deploymentpb.RoutingConfig{
CurrentDeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: "foo", BuildId: "C"},
CurrentVersionChangedTime: timestamppb.Now(),
RevisionNumber: 1,
}
_, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: "foo",
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW, enumspb.TASK_QUEUE_TYPE_ACTIVITY},
UpdateRoutingConfig: rc,
})
s.NoError(err)
// Fetch and verify that old-format versions under the same deployment (foo) are removed, others remain
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
//nolint:staticcheck // SA1019 deprecated versions will clean up later
got := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetVersions()
s.Require().Len(got, 1)
s.Equal("bar", got[0].GetVersion().GetDeploymentName())
s.Equal("B", got[0].GetVersion().GetBuildId())
}
func (s *matchingEngineSuite) TestSyncDeploymentUserData_RoutingConfigRevisionGating() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
rc1 := &deploymentpb.RoutingConfig{
CurrentDeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: "foo", BuildId: "rev1"},
RevisionNumber: 1,
CurrentVersionChangedTime: timestamppb.Now(),
}
resp, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: "foo",
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpdateRoutingConfig: rc1,
})
s.NoError(err)
s.True(resp.GetRoutingConfigChanged())
// Attempt to sync lower revision (0) — should be ignored
rc0 := &deploymentpb.RoutingConfig{
CurrentDeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: "foo", BuildId: "rev0"},
RevisionNumber: 0,
CurrentVersionChangedTime: timestamppb.Now(),
}
resp, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: "foo",
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpdateRoutingConfig: rc0,
})
s.NoError(err)
s.False(resp.GetRoutingConfigChanged())
// Verify still rev 1
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
gotRC := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()["foo"].GetRoutingConfig()
s.Equal(int64(1), gotRC.GetRevisionNumber())
s.Equal("rev1", gotRC.GetCurrentDeploymentVersion().GetBuildId())
// Now sync higher revision (2) — should be applied
rc2 := &deploymentpb.RoutingConfig{
CurrentDeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: "foo", BuildId: "rev2"},
RevisionNumber: 2,
CurrentVersionChangedTime: timestamppb.Now(),
}
resp, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: "foo",
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpdateRoutingConfig: rc2,
})
s.NoError(err)
s.True(resp.GetRoutingConfigChanged())
res, err = s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
gotRC = res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()["foo"].GetRoutingConfig()
s.Equal(int64(2), gotRC.GetRevisionNumber())
s.Equal("rev2", gotRC.GetCurrentDeploymentVersion().GetBuildId())
}
func (s *matchingEngineSuite) TestSyncDeploymentUserData_UpsertAndForgetVersions_WithoutRoutingConfig() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
deploymentName := "foo"
// Upsert versions without providing RoutingConfig
upserts1 := map[string]*deploymentspb.WorkerDeploymentVersionData{
"b1": {},
"b2": {},
}
resp, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: upserts1,
// No UpdateRoutingConfig provided
})
s.NoError(err)
s.False(resp.GetRoutingConfigChanged())
// Verify both b1 and b2 exist
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
versions := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetVersions()
s.Contains(versions, "b1")
s.Contains(versions, "b2")
s.Len(versions, 2)
// Forget b1 and upsert b3, still without RoutingConfig
upserts2 := map[string]*deploymentspb.WorkerDeploymentVersionData{
"b3": {},
}
resp, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: upserts2,
ForgetVersions: []string{"b1"},
// No UpdateRoutingConfig provided
})
s.NoError(err)
s.False(resp.GetRoutingConfigChanged())
// Verify b1 removed, b2 remains, b3 added
res, err = s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
versions = res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetVersions()
s.NotContains(versions, "b1")
s.Contains(versions, "b2")
s.Contains(versions, "b3")
s.Len(versions, 2)
}
//nolint:staticcheck // SA1019 deprecated versions will clean up later
func (s *matchingEngineSuite) TestSyncDeploymentUserData_UnsetOldFormatRamp_ClearsNewFormatRamp() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
deploymentName := "foo"
// New-format ramp for deployment "foo"
rc := &deploymentpb.RoutingConfig{
RampingDeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: deploymentName, BuildId: "b1"},
RampingVersionPercentage: 25,
RampingVersionPercentageChangedTime: timestamppb.Now(),
RevisionNumber: 1,
}
resp, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpdateRoutingConfig: rc,
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
"b1": {
Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING,
},
},
})
s.NoError(err)
s.True(resp.GetRoutingConfigChanged())
// Verify ramp present in new-format data
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
rcGot := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetRoutingConfig()
s.NotNil(rcGot.GetRampingDeploymentVersion())
s.Equal("b1", rcGot.GetRampingDeploymentVersion().GetBuildId())
s.InDelta(float32(25), rcGot.GetRampingVersionPercentage(), 0.0000)
// Now unset ramp via old-format unversioned ramp (Version=nil, RampingSinceTime=nil)
unsetVD := &deploymentspb.DeploymentVersionData{
// Version: nil (unversioned)
// RampingSinceTime: nil indicates unset
}
resp, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
Operation: &matchingservice.SyncDeploymentUserDataRequest_UpdateVersionData{
UpdateVersionData: unsetVD,
},
})
s.NoError(err)
// Verify unversioned ramp cleared and new-format ramp cleared
res, err = s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
depData := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData()
// old-format unversioned ramp should be nil
s.Nil(depData.GetUnversionedRampData())
// new-format ramp entries should be cleared
rcGot = depData.GetDeploymentsData()[deploymentName].GetRoutingConfig()
s.Nil(rcGot.GetRampingDeploymentVersion())
s.InDelta(float32(0), rcGot.GetRampingVersionPercentage(), 0.0000)
s.Nil(rcGot.GetRampingVersionPercentageChangedTime())
s.Nil(rcGot.GetRampingVersionChangedTime())
}
//nolint:staticcheck // SA1019 deprecated versions will clean up later
func (s *matchingEngineSuite) TestSyncDeploymentUserData_OldFormatSetSameCurrent_ClearsNewFormatCurrent() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
deploymentName := "foo"
// set current=v1
rc := &deploymentpb.RoutingConfig{
CurrentDeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: deploymentName, BuildId: "v1"},
CurrentVersionChangedTime: timestamppb.Now(),
RevisionNumber: 1,
}
resp, err := s.matchingEngine.SyncDeploymentUserData(
context.Background(),
&matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpdateRoutingConfig: rc,
},
)
s.NoError(err)
s.True(resp.GetRoutingConfigChanged())
// Verify current=v1 present in new-format
res, err := s.matchingEngine.GetTaskQueueUserData(
context.Background(),
&matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
},
)
s.NoError(err)
rcGot := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].
GetDeploymentData().GetDeploymentsData()[deploymentName].GetRoutingConfig()
s.Equal("v1", rcGot.GetCurrentDeploymentVersion().GetBuildId())
// Old-format: set SAME version (v1) as current -> should clear new-format current
vdCurrent := &deploymentspb.DeploymentVersionData{
Version: &deploymentspb.WorkerDeploymentVersion{DeploymentName: deploymentName, BuildId: "v1"},
CurrentSinceTime: timestamppb.Now(),
RoutingUpdateTime: timestamppb.Now(),
}
_, err = s.matchingEngine.SyncDeploymentUserData(
context.Background(),
&matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
Operation: &matchingservice.SyncDeploymentUserDataRequest_UpdateVersionData{
UpdateVersionData: vdCurrent,
},
},
)
s.NoError(err)
// Verify new-format current cleared
res, err = s.matchingEngine.GetTaskQueueUserData(
context.Background(),
&matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
},
)
s.NoError(err)
rcGot = res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].
GetDeploymentData().GetDeploymentsData()[deploymentName].GetRoutingConfig()
s.Nil(rcGot.GetCurrentDeploymentVersion())
}
//nolint:staticcheck // SA1019 deprecated versions will clean up later
func (s *matchingEngineSuite) TestSyncDeploymentUserData_ForgetNewFormat_RemovesOldFormatMembership() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
deploymentName := "foo"
buildID := "b1"
// Upsert b1 using the old format
_, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
Operation: &matchingservice.SyncDeploymentUserDataRequest_UpdateVersionData{
UpdateVersionData: &deploymentspb.DeploymentVersionData{
Version: &deploymentspb.WorkerDeploymentVersion{DeploymentName: deploymentName, BuildId: buildID},
RoutingUpdateTime: timestamppb.Now(),
},
},
})
s.NoError(err)
// Upsert b1 using the new format
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
buildID: {},
},
})
s.NoError(err)
// Forget via new-format
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
ForgetVersions: []string{buildID},
UpdateRoutingConfig: nil,
})
s.NoError(err)
// Verify removal from both formats
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
depData := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData()
// new-format membership
_, exists := depData.GetDeploymentsData()[deploymentName].GetVersions()[buildID]
s.False(exists)
// old-format slice
for _, dv := range depData.GetVersions() {
s.NotEqual(buildID, dv.GetVersion().GetBuildId())
}
// and the deployment entry should be removed from DeploymentsData as versions map is now empty
_, deploymentExists := depData.GetDeploymentsData()[deploymentName]
s.False(deploymentExists)
}
//nolint:staticcheck // SA1019 deprecated versions will clean up later
func (s *matchingEngineSuite) TestSyncDeploymentUserData_ForgetOldFormat_RemovesNewFormatMembership() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
deploymentName := "foo"
buildID := "b1"
// Upsert b1 using the old format
_, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
Operation: &matchingservice.SyncDeploymentUserDataRequest_UpdateVersionData{
UpdateVersionData: &deploymentspb.DeploymentVersionData{
Version: &deploymentspb.WorkerDeploymentVersion{DeploymentName: deploymentName, BuildId: buildID},
RoutingUpdateTime: timestamppb.Now(),
},
},
})
s.NoError(err)
// Upsert b1 using the new format (membership map)
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
buildID: {},
},
})
s.NoError(err)
// Forget via old-format
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
Operation: &matchingservice.SyncDeploymentUserDataRequest_ForgetVersion{
ForgetVersion: &deploymentspb.WorkerDeploymentVersion{DeploymentName: deploymentName, BuildId: buildID},
},
})
s.NoError(err)
// Verify removal from both formats
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
depData := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData()
// new-format membership map
_, exists := depData.GetDeploymentsData()[deploymentName].GetVersions()[buildID]
s.False(exists)
// old-format slice
for _, dv := range depData.GetVersions() {
s.NotEqual(buildID, dv.GetVersion().GetBuildId())
}
// the deployment entry should be removed from DeploymentsData as versions map is now empty
_, deploymentExists := depData.GetDeploymentsData()[deploymentName]
s.False(deploymentExists)
}
func (s *matchingEngineSuite) setupRecordActivityTaskStartedMock(tlName string) {
activityTypeName := "activity1"
activityID := "activityId1"
activityType := &commonpb.ActivityType{Name: activityTypeName}
activityInput := payloads.EncodeString("Activity1 Input")
// History service is using mock
s.mockHistoryClient.EXPECT().RecordActivityTaskStarted(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, taskRequest *historyservice.RecordActivityTaskStartedRequest, arg2 ...any) (*historyservice.RecordActivityTaskStartedResponse, error) {
s.logger.Debug("Mock Received RecordActivityTaskStartedRequest")
return &historyservice.RecordActivityTaskStartedResponse{
Attempt: 1,
ScheduledEvent: newActivityTaskScheduledEvent(taskRequest.ScheduledEventId, 0,
&commandpb.ScheduleActivityTaskCommandAttributes{
ActivityId: activityID,
TaskQueue: &taskqueuepb.TaskQueue{
Name: tlName,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
ActivityType: activityType,
Input: activityInput,
ScheduleToCloseTimeout: durationpb.New(100 * time.Second),
ScheduleToStartTimeout: durationpb.New(50 * time.Second),
StartToCloseTimeout: durationpb.New(50 * time.Second),
HeartbeatTimeout: durationpb.New(10 * time.Second),
}),
}, nil
}).AnyTimes()
}
func newActivityTaskScheduledEvent(eventID int64, workflowTaskCompletedEventID int64,
scheduleAttributes *commandpb.ScheduleActivityTaskCommandAttributes,
) *historypb.HistoryEvent {
historyEvent := newHistoryEvent(eventID, enumspb.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED)
historyEvent.Attributes = &historypb.HistoryEvent_ActivityTaskScheduledEventAttributes{ActivityTaskScheduledEventAttributes: &historypb.ActivityTaskScheduledEventAttributes{
ActivityId: scheduleAttributes.ActivityId,
ActivityType: scheduleAttributes.ActivityType,
TaskQueue: scheduleAttributes.TaskQueue,
Input: scheduleAttributes.Input,
Header: scheduleAttributes.Header,
ScheduleToCloseTimeout: scheduleAttributes.ScheduleToCloseTimeout,
ScheduleToStartTimeout: scheduleAttributes.ScheduleToStartTimeout,
StartToCloseTimeout: scheduleAttributes.StartToCloseTimeout,
HeartbeatTimeout: scheduleAttributes.HeartbeatTimeout,
WorkflowTaskCompletedEventId: workflowTaskCompletedEventID,
}}
return historyEvent
}
func (s *matchingEngineSuite) TestSyncDeploymentUserData_UpdateRoutingConfig_MigratesOldVersionsToNewFormat() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
depFoo := "foo"
depBar := "bar"
// Add old-format versions: foo:A, foo:B, bar:C
addOldFormatVersion := func(dep, build string) {
_, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: dep,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
Operation: &matchingservice.SyncDeploymentUserDataRequest_UpdateVersionData{
UpdateVersionData: &deploymentspb.DeploymentVersionData{
Version: &deploymentspb.WorkerDeploymentVersion{DeploymentName: dep, BuildId: build},
RoutingUpdateTime: timestamppb.Now(),
},
},
})
s.NoError(err)
}
addOldFormatVersion(depFoo, "A")
addOldFormatVersion(depFoo, "B")
addOldFormatVersion(depBar, "C")
// Apply new-format routing config for foo with higher revision to trigger migration
rc := &deploymentpb.RoutingConfig{
CurrentDeploymentVersion: &deploymentpb.WorkerDeploymentVersion{DeploymentName: depFoo, BuildId: "A"},
CurrentVersionChangedTime: timestamppb.Now(),
RevisionNumber: 1,
}
_, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: depFoo,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpdateRoutingConfig: rc,
})
s.NoError(err)
// Verify: old-format slice retains only bar:C; foo:A & foo:B migrated
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
depData := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData()
// Old-format slice check
var oldSliceBuilds []string
//nolint:staticcheck // SA1019 deprecated versions will clean up later
for _, dv := range depData.GetVersions() {
if dv.GetVersion().GetDeploymentName() == depBar {
oldSliceBuilds = append(oldSliceBuilds, dv.GetVersion().GetBuildId())
}
// Ensure none for foo remain
s.NotEqual(depFoo, dv.GetVersion().GetDeploymentName())
}
s.ElementsMatch([]string{"C"}, oldSliceBuilds)
// New-format membership map for foo should contain A & B
fooVersions := depData.GetDeploymentsData()[depFoo].GetVersions()
_, hasA := fooVersions["A"]
_, hasB := fooVersions["B"]
s.True(hasA)
s.True(hasB)
// And not C
_, hasC := fooVersions["C"]
s.False(hasC)
}
func (s *matchingEngineSuite) TestSyncDeploymentUserData_VersionDataRevisionGating() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
deploymentName := "foo"
buildID := "v1"
// Upsert version data with revision 1
versionData1 := &deploymentspb.WorkerDeploymentVersionData{
RevisionNumber: 1,
Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT,
}
_, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
buildID: versionData1,
},
})
s.NoError(err)
// Verify version data is stored
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
versions := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetVersions()
s.Require().Contains(versions, buildID)
s.Equal(int64(1), versions[buildID].GetRevisionNumber())
s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, versions[buildID].GetStatus())
// Attempt to upsert with lower revision (0) — should be ignored
versionData0 := &deploymentspb.WorkerDeploymentVersionData{
RevisionNumber: 0,
Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING,
}
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
buildID: versionData0,
},
})
s.NoError(err)
// Verify revision still 1 and status unchanged
res, err = s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
versions = res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetVersions()
s.Equal(int64(1), versions[buildID].GetRevisionNumber())
s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, versions[buildID].GetStatus())
// Upsert with equal revision (1) — should be accepted
versionData1Equal := &deploymentspb.WorkerDeploymentVersionData{
RevisionNumber: 1,
Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING,
}
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
buildID: versionData1Equal,
},
})
s.NoError(err)
// Verify status changed even though revision is equal
res, err = s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
versions = res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetVersions()
s.Equal(int64(1), versions[buildID].GetRevisionNumber())
s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_RAMPING, versions[buildID].GetStatus())
// Upsert with higher revision (2) — should be accepted
versionData2 := &deploymentspb.WorkerDeploymentVersionData{
RevisionNumber: 2,
Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT,
}
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
buildID: versionData2,
},
})
s.NoError(err)
// Verify revision updated
res, err = s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
versions = res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetVersions()
s.Equal(int64(2), versions[buildID].GetRevisionNumber())
s.Equal(enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT, versions[buildID].GetStatus())
}
//nolint:staticcheck // SA1019 deprecated versions will clean up later
func (s *matchingEngineSuite) TestSyncDeploymentUserData_DeletedVersionRemovesOldFormat() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
deploymentName := "foo"
buildID := "v1"
// Add old-format version
t1 := timestamppb.Now()
userData := &persistencespb.VersionedTaskQueueUserData{
Version: 1,
Data: &persistencespb.TaskQueueUserData{
PerType: map[int32]*persistencespb.TaskQueueTypeUserData{
int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW): {
DeploymentData: &persistencespb.DeploymentData{
Versions: []*deploymentspb.DeploymentVersionData{
{
Version: &deploymentspb.WorkerDeploymentVersion{DeploymentName: deploymentName, BuildId: buildID},
RoutingUpdateTime: t1,
CurrentSinceTime: t1,
},
},
},
},
},
},
}
s.NoError(s.classicTaskManager.UpdateTaskQueueUserData(context.Background(), &persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceID,
Updates: map[string]*persistence.SingleTaskQueueUserDataUpdate{
tq: {UserData: userData},
},
}))
// Verify old-format version exists
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
//nolint:staticcheck // SA1019 deprecated versions will clean up later
oldVersions := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetVersions()
s.Require().Len(oldVersions, 1)
s.Equal(buildID, oldVersions[0].GetVersion().GetBuildId())
// Mark version as deleted in new format (with recent update time so it doesn't get cleaned up)
deletedVersionData := &deploymentspb.WorkerDeploymentVersionData{
RevisionNumber: 1,
Deleted: true,
UpdateTime: timestamppb.Now(), // Set recent update time to prevent cleanup
}
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
buildID: deletedVersionData,
},
})
s.NoError(err)
// Verify old-format version is removed
res, err = s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
//nolint:staticcheck // SA1019 deprecated versions will clean up later
oldVersions = res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetVersions()
s.Empty(oldVersions)
// Verify deleted version still exists in new format
versions := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetVersions()
s.Require().Contains(versions, buildID)
s.True(versions[buildID].GetDeleted())
}
func (s *matchingEngineSuite) TestSyncDeploymentUserData_CleanupOldDeletedVersions() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
deploymentName := "foo"
// First, insert versions with recent update times
recentTime := timestamppb.New(time.Now().Add(-6 * 24 * time.Hour)) // 6 days old (should remain)
versionsData := map[string]*deploymentspb.WorkerDeploymentVersionData{
"to-be-old-build": {
RevisionNumber: 1,
Deleted: true,
UpdateTime: recentTime, // Start with recent time
},
"recent-build": {
RevisionNumber: 1,
Deleted: true,
UpdateTime: recentTime,
},
"active-build": {
RevisionNumber: 1,
Status: enumspb.WORKER_DEPLOYMENT_VERSION_STATUS_CURRENT,
UpdateTime: recentTime,
},
}
// Insert all versions
_, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: versionsData,
})
s.NoError(err)
// Verify all versions are present
res, err := s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
versions := res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetVersions()
s.Len(versions, 3)
s.Contains(versions, "to-be-old-build")
s.Contains(versions, "recent-build")
s.Contains(versions, "active-build")
// Now update "to-be-old-build" to have an old timestamp (8 days)
oldTime := timestamppb.New(time.Now().Add(-8 * 24 * time.Hour))
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
"to-be-old-build": {
RevisionNumber: 2, // Higher revision to allow update
Deleted: true,
UpdateTime: oldTime,
},
},
})
s.NoError(err)
// Trigger cleanup by syncing again (cleanupOldDeletedVersions is called during each sync)
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
// Empty sync to trigger cleanup
})
s.NoError(err)
// Verify only old deleted version is removed
res, err = s.matchingEngine.GetTaskQueueUserData(context.Background(), &matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
LastKnownUserDataVersion: 0,
})
s.NoError(err)
versions = res.GetUserData().GetData().GetPerType()[int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW)].GetDeploymentData().GetDeploymentsData()[deploymentName].GetVersions()
s.Len(versions, 2)
s.NotContains(versions, "to-be-old-build") // Old deleted version should be removed
s.Contains(versions, "recent-build") // Recent deleted version should remain
s.Contains(versions, "active-build") // Active version should remain
s.True(versions["recent-build"].GetDeleted())
s.False(versions["active-build"].GetDeleted())
}
func (s *matchingEngineSuite) TestSyncDeploymentUserData_UpsertedVersionsData() {
tv := testvars.New(s.T())
namespaceID := tv.NamespaceID().String()
tq := tv.TaskQueue().GetName()
deploymentName := "foo"
// Test single upsert
_, err := s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
"v1": {RevisionNumber: 1},
},
})
s.NoError(err)
// Test multiple upserts
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
"v2": {RevisionNumber: 1},
"v3": {RevisionNumber: 1},
},
})
s.NoError(err)
// Test upsert with one stale version (should not be included in response)
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
UpsertVersionsData: map[string]*deploymentspb.WorkerDeploymentVersionData{
"v1": {RevisionNumber: 0}, // Stale, should be rejected
"v4": {RevisionNumber: 1}, // New, should be accepted
},
})
s.NoError(err)
// Test no upserts
_, err = s.matchingEngine.SyncDeploymentUserData(context.Background(), &matchingservice.SyncDeploymentUserDataRequest{
NamespaceId: namespaceID,
TaskQueue: tq,
DeploymentName: deploymentName,
TaskQueueTypes: []enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW},
})
s.NoError(err)
}
func newHistoryEvent(eventID int64, eventType enumspb.EventType) *historypb.HistoryEvent {
return &historypb.HistoryEvent{
EventId: eventID,
EventTime: timestamppb.New(time.Now().UTC()),
EventType: eventType,
}
}
var _ persistence.TaskManager = (*testTaskManager)(nil) // Asserts that interface is indeed implemented
var _ persistence.FairTaskManager = (*testTaskManager)(nil) // Asserts that interface is indeed implemented
type testTaskManager struct {
sync.Mutex
queues map[dbTaskQueueKey]*testQueueData
logger log.Logger
fairness bool
faultInjection map[string]float32 // "op:error" -> fraction of time
delayInjection time.Duration
}
type dbTaskQueueKey struct {
persistenceName string
namespaceID string
taskType enumspb.TaskQueueType
}
func newTestTaskManager(logger log.Logger) *testTaskManager {
return &testTaskManager{
queues: make(map[dbTaskQueueKey]*testQueueData),
logger: logger,
}
}
func newTestFairTaskManager(logger log.Logger) *testTaskManager {
m := newTestTaskManager(logger)
m.fairness = true
return m
}
func (m *testTaskManager) GetName() string {
return "test"
}
func (m *testTaskManager) Close() {
}
func (m *testTaskManager) getQueueDataByKey(dbq *PhysicalTaskQueueKey) *testQueueData {
return m.getQueueData(dbq.PersistenceName(), dbq.NamespaceId(), dbq.TaskType())
}
func (m *testTaskManager) getQueueData(name, namespaceID string, taskType enumspb.TaskQueueType) *testQueueData {
key := dbTaskQueueKey{persistenceName: name, namespaceID: namespaceID, taskType: taskType}
m.Lock()
defer m.Unlock()
if queue, ok := m.queues[key]; ok {
return queue
}
queue := newTestQueueData()
m.queues[key] = queue
return queue
}
func newUnversionedRootQueueKey(namespaceID string, name string, taskType enumspb.TaskQueueType) *PhysicalTaskQueueKey {
return UnversionedQueueKey(newTestTaskQueue(namespaceID, name, taskType).RootPartition())
}
func newRootPartition(namespaceID string, name string, taskType enumspb.TaskQueueType) *tqid.NormalPartition {
return newTestTaskQueue(namespaceID, name, taskType).RootPartition()
}
func newTestTaskQueue(namespaceID string, name string, taskType enumspb.TaskQueueType) *tqid.TaskQueue {
result, err := tqid.NewTaskQueueFamily(namespaceID, name)
if err != nil {
panic(fmt.Sprintf("newTaskQueueID failed with error %v", err))
}
return result.TaskQueue(taskType)
}
type testQueueData struct {
sync.Mutex
rangeID int64
info *persistencespb.TaskQueueInfo
tasks treemap.Map
userData *persistencespb.VersionedTaskQueueUserData
testQueuePersistenceStats
}
type testQueuePersistenceStats struct {
createTaskCount int
createTaskBatchCount int
getTasksCount int
getUserDataCount int
createCount int
updateCount int
}
func newTestQueueData() *testQueueData {
return &testQueueData{tasks: *newFairLevelTreeMap()}
}
func (q *testQueueData) String() string {
var out strings.Builder
q.Lock()
defer q.Unlock()
fmt.Fprintf(&out, "<task queue ")
fmt.Fprintf(&out, "rangeID: %d ", q.rangeID)
fmt.Fprintf(&out, "info: %s ", q.info.String())
fmt.Fprintf(&out, "tasks: %d>", q.tasks.Size())
return out.String()
}
func (q *testQueueData) RangeID() int64 {
q.Lock()
defer q.Unlock()
return q.rangeID
}
func (q *testQueueData) persistenceStats() testQueuePersistenceStats {
q.Lock()
defer q.Unlock()
return q.testQueuePersistenceStats
}
func (m *testTaskManager) CreateTaskQueue(
_ context.Context,
request *persistence.CreateTaskQueueRequest,
) (*persistence.CreateTaskQueueResponse, error) {
tli := request.TaskQueueInfo
tlm := m.getQueueData(tli.Name, tli.NamespaceId, tli.TaskType)
m.delay()
defer m.delay()
tlm.Lock()
defer tlm.Unlock()
tlm.createCount++
if tlm.rangeID != 0 {
return nil, &persistence.ConditionFailedError{
Msg: fmt.Sprintf("Failed to create task queue: name=%v, type=%v", tli.Name, tli.TaskType),
}
}
tlm.rangeID = request.RangeID
tlm.info = common.CloneProto(tli)
return &persistence.CreateTaskQueueResponse{}, nil
}
func (m *testTaskManager) UpdateTaskQueue(
_ context.Context,
request *persistence.UpdateTaskQueueRequest,
) (*persistence.UpdateTaskQueueResponse, error) {
tli := request.TaskQueueInfo
tlm := m.getQueueData(tli.Name, tli.NamespaceId, tli.TaskType)
m.delay()
defer m.delay()
tlm.Lock()
defer tlm.Unlock()
tlm.updateCount++
if tlm.rangeID != request.PrevRangeID {
return nil, &persistence.ConditionFailedError{
Msg: fmt.Sprintf("Failed to update task queue: name=%v, type=%v", tli.Name, tli.TaskType),
}
}
tlm.rangeID = request.RangeID
tlm.info = common.CloneProto(tli)
return &persistence.UpdateTaskQueueResponse{}, nil
}
func (m *testTaskManager) GetTaskQueue(
_ context.Context,
request *persistence.GetTaskQueueRequest,
) (*persistence.GetTaskQueueResponse, error) {
tlm := m.getQueueData(request.TaskQueue, request.NamespaceID, request.TaskType)
tlm.Lock()
defer tlm.Unlock()
if tlm.rangeID == 0 {
return nil, serviceerror.NewNotFound("task queue not found")
}
return &persistence.GetTaskQueueResponse{
RangeID: tlm.rangeID,
TaskQueueInfo: common.CloneProto(tlm.info),
}, nil
}
// minTaskID returns the minimum value of the TaskID present in testTaskManager
func (m *testTaskManager) minTaskID(dbq *PhysicalTaskQueueKey) (int64, bool) {
tlm := m.getQueueDataByKey(dbq)
tlm.Lock()
defer tlm.Unlock()
minKey, _ := tlm.tasks.Min()
key, ok := minKey.(fairLevel)
return key.id, ok
}
// maxTaskID returns the maximum value of the TaskID present in testTaskManager
func (m *testTaskManager) maxTaskID(dbq *PhysicalTaskQueueKey) (int64, bool) {
tlm := m.getQueueDataByKey(dbq)
tlm.Lock()
defer tlm.Unlock()
maxKey, _ := tlm.tasks.Max()
key, ok := maxKey.(fairLevel)
return key.id, ok
}
func (m *testTaskManager) CompleteTasksLessThan(
_ context.Context,
request *persistence.CompleteTasksLessThanRequest,
) (int, error) {
if m.fairness && request.ExclusiveMaxPass < 1 {
return 0, serviceerror.NewInternal("invalid CompleteTasksLessThan request on fair queue")
} else if !m.fairness && request.ExclusiveMaxPass != 0 {
return 0, serviceerror.NewInternal("invalid CompleteTasksLessThan request on queue")
}
m.delay()
defer m.delay()
tlm := m.getQueueData(request.TaskQueueName, request.NamespaceID, request.TaskType)
tlm.Lock()
defer tlm.Unlock()
keys := tlm.tasks.Keys()
for _, key := range keys {
level := key.(fairLevel)
if m.fairness {
if level.less(fairLevel{pass: request.ExclusiveMaxPass, id: request.ExclusiveMaxTaskID}) {
tlm.tasks.Remove(level)
}
} else {
if level.id < request.ExclusiveMaxTaskID {
tlm.tasks.Remove(level)
}
}
}
return persistence.UnknownNumRowsAffected, nil
}
func (m *testTaskManager) ListTaskQueue(
_ context.Context,
_ *persistence.ListTaskQueueRequest,
) (*persistence.ListTaskQueueResponse, error) {
return nil, fmt.Errorf("unsupported operation")
}
func (m *testTaskManager) DeleteTaskQueue(
_ context.Context,
request *persistence.DeleteTaskQueueRequest,
) error {
m.Lock()
defer m.Unlock()
key := dbTaskQueueKey{persistenceName: request.TaskQueue.TaskQueueName, namespaceID: request.TaskQueue.NamespaceID, taskType: request.TaskQueue.TaskQueueType}
delete(m.queues, key)
return nil
}
func (m *testTaskManager) delay() {
if m.delayInjection > 0 && rand.Int31n(128) >= 13 {
time.Sleep(time.Duration(rand.Float32() * float32(m.delayInjection))) // nolint:forbidigo
}
}
// all calls to addFault should be done before starting to call methods on testTaskManager
func (m *testTaskManager) addFault(method, err string, fraction float32) {
if m.faultInjection == nil {
m.faultInjection = make(map[string]float32)
}
m.faultInjection[method+":"+err] = fraction
}
func (m *testTaskManager) fault(method, err string) bool {
return rand.Float32() < m.faultInjection[method+":"+err]
}
func (m *testTaskManager) CreateTasks(
_ context.Context,
request *persistence.CreateTasksRequest,
) (*persistence.CreateTasksResponse, error) {
namespaceID := request.TaskQueueInfo.Data.GetNamespaceId()
taskQueue := request.TaskQueueInfo.Data.Name
taskType := request.TaskQueueInfo.Data.TaskType
rangeID := request.TaskQueueInfo.RangeID
m.delay()
defer m.delay()
if m.fault("CreateTasks", "ConditionFailed") {
return nil, &persistence.ConditionFailedError{Msg: "Fake ConditionFailedError"}
} else if m.fault("CreateTasks", "Unavailable") {
return nil, serviceerror.NewUnavailable("Fake Unavailable")
}
tlm := m.getQueueData(taskQueue, namespaceID, taskType)
tlm.Lock()
defer tlm.Unlock()
if tlm.rangeID != rangeID {
m.logger.Debug("testTaskManager.CreateTask ConditionFailedError",
tag.ShardRangeID(rangeID), tag.ShardRangeID(tlm.rangeID))
return nil, &persistence.ConditionFailedError{
Msg: fmt.Sprintf("CreateTask failed, range id mismatch. TaskQueue: %v, taskQueueType: %v, rangeID: %v, db rangeID: %v",
taskQueue, taskType, rangeID, tlm.rangeID),
}
}
// First validate the entire batch
for _, task := range request.Tasks {
level := fairLevelFromAllocatedTask(task)
m.logger.Debug("testTaskManager.CreateTask", tag.ShardRangeID(rangeID), tag.TaskKey(level), tag.Value(task.Data))
if task.GetTaskId() <= 0 {
panic(fmt.Errorf("invalid taskID=%v", task.GetTaskId()))
}
if m.fairness && task.TaskPass == 0 {
return nil, serviceerror.NewInternal("invalid fair queue task missing pass number")
} else if !m.fairness && task.TaskPass != 0 {
return nil, serviceerror.NewInternal("invalid non-fair queue task with pass number")
}
if _, ok := tlm.tasks.Get(level); ok {
panic(fmt.Sprintf("Duplicated TaskID %v", level))
}
}
// Then insert all tasks if no errors
for _, task := range request.Tasks {
tlm.tasks.Put(fairLevelFromAllocatedTask(task), common.CloneProto(task))
tlm.createTaskCount++
}
tlm.createTaskBatchCount++
resp := &persistence.CreateTasksResponse{}
if request.UpdateMetadata {
tlm.info = common.CloneProto(request.TaskQueueInfo.Data)
resp.UpdatedMetadata = true
}
return resp, nil
}
func (m *testTaskManager) GetTasks(
_ context.Context,
request *persistence.GetTasksRequest,
) (*persistence.GetTasksResponse, error) {
m.logger.Debug("testTaskManager.GetTasks", tag.Value(request))
if m.fairness && (request.InclusiveMinPass < 1 || request.ExclusiveMaxTaskID != math.MaxInt64) {
return nil, serviceerror.NewInternal("invalid GetTasks request on fair queue")
} else if !m.fairness && request.InclusiveMinPass != 0 {
return nil, serviceerror.NewInternal("invalid GetTasks request on queue")
}
m.delay()
defer m.delay()
if m.fault("GetTasks", "Unavailable") {
return nil, serviceerror.NewUnavailablef("GetTasks operation failed")
}
tlm := m.getQueueData(request.TaskQueue, request.NamespaceID, request.TaskType)
tlm.Lock()
defer tlm.Unlock()
var tasks []*persistencespb.AllocatedTaskInfo
it := tlm.tasks.Iterator()
for it.Next() && len(tasks) < request.PageSize {
level := it.Key().(fairLevel)
if m.fairness {
if level.less(fairLevel{pass: request.InclusiveMinPass, id: request.InclusiveMinTaskID}) {
continue
}
} else {
if level.id < request.InclusiveMinTaskID {
continue
}
if level.id >= request.ExclusiveMaxTaskID {
break
}
}
tasks = append(tasks, it.Value().(*persistencespb.AllocatedTaskInfo))
}
tlm.getTasksCount++
return &persistence.GetTasksResponse{Tasks: tasks}, nil
}
// getTaskCount returns number of tasks in a task queue
func (m *testTaskManager) getTaskCount(q *PhysicalTaskQueueKey) int {
tlm := m.getQueueDataByKey(q)
tlm.Lock()
defer tlm.Unlock()
return tlm.tasks.Size()
}
// getCreateTaskCount returns how many tasks were added
func (m *testTaskManager) getCreateTaskCount(q *PhysicalTaskQueueKey) int {
tlm := m.getQueueDataByKey(q)
tlm.Lock()
defer tlm.Unlock()
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)
tlm.Lock()
defer tlm.Unlock()
return tlm.getTasksCount
}
// getGetUserDataCount returns how many times GetUserData was called
func (m *testTaskManager) getGetUserDataCount(q *PhysicalTaskQueueKey) int {
tlm := m.getQueueDataByKey(q)
tlm.Lock()
defer tlm.Unlock()
return tlm.getUserDataCount
}
// getUpdateCount returns how many times UpdateTaskQueue was called
func (m *testTaskManager) getUpdateCount(q *PhysicalTaskQueueKey) int {
tlm := m.getQueueDataByKey(q)
tlm.Lock()
defer tlm.Unlock()
return tlm.updateCount
}
func (m *testTaskManager) String() string {
m.Lock()
defer m.Unlock()
var out strings.Builder
fmt.Fprintf(&out, "testTaskManager: %d queues:\n", len(m.queues))
for k, q := range m.queues {
fmt.Fprintf(&out, " %s %s: %s\n", k.persistenceName, k.taskType, q)
}
return out.String()
}
// GetTaskQueueData implements persistence.TaskManager
func (m *testTaskManager) GetTaskQueueUserData(_ context.Context, request *persistence.GetTaskQueueUserDataRequest) (*persistence.GetTaskQueueUserDataResponse, error) {
if m.fairness {
panic("userdata calls should not to go fair task manager")
}
tlm := m.getQueueData(request.TaskQueue, request.NamespaceID, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
tlm.Lock()
defer tlm.Unlock()
tlm.getUserDataCount++
return &persistence.GetTaskQueueUserDataResponse{
UserData: tlm.userData,
}, nil
}
// UpdateTaskQueueUserData implements persistence.TaskManager
func (m *testTaskManager) UpdateTaskQueueUserData(_ context.Context, request *persistence.UpdateTaskQueueUserDataRequest) error {
if m.fairness {
panic("userdata calls should not to go fair task manager")
}
for tq, update := range request.Updates {
tlm := m.getQueueData(tq, request.NamespaceID, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
tlm.Lock()
newData := common.CloneProto(update.UserData)
newData.Version++
tlm.userData = newData
tlm.Unlock()
}
return nil
}
// ListTaskQueueUserDataEntries implements persistence.TaskManager
func (*testTaskManager) ListTaskQueueUserDataEntries(context.Context, *persistence.ListTaskQueueUserDataEntriesRequest) (*persistence.ListTaskQueueUserDataEntriesResponse, error) {
// No need to implement this for unit tests
panic("unimplemented")
}
// GetTaskQueuesByBuildId implements persistence.TaskManager
func (*testTaskManager) GetTaskQueuesByBuildId(context.Context, *persistence.GetTaskQueuesByBuildIdRequest) ([]string, error) {
// No need to implement this for unit tests
panic("unimplemented")
}
// CountTaskQueuesByBuildId implements persistence.TaskManager
func (*testTaskManager) CountTaskQueuesByBuildId(context.Context, *persistence.CountTaskQueuesByBuildIdRequest) (int, error) {
// This is only used to validate that the build ID to task queue mapping is enforced (at the time of writing), report 0.
return 0, nil
}
// TestLoggerAndMetricsForPartition_BreakdownEnabled verifies the taskqueue and partition metric
// tags for each task queue kind with the default BreakdownMetricsByTaskQueue=true.
func TestLoggerAndMetricsForPartition_BreakdownEnabled(t *testing.T) {
t.Parallel()
controller := gomock.NewController(t)
ns, mockNamespaceCache := createMockNamespaceCache(controller, matchingTestNamespace)
config := defaultTestConfig()
e := createTestMatchingEngine(log.NewTestLogger(), controller, config, nil, mockNamespaceCache)
captureHandler := metricstest.NewCaptureHandler()
e.metricsHandler = captureHandler
tests := []struct {
name string
partition tqid.Partition
expectTQValue string
expectPartitionTag string
}{
{
name: "normal",
partition: newRootPartition(ns.ID().String(), "my-task-queue", enumspb.TASK_QUEUE_TYPE_NEXUS),
expectTQValue: "my-task-queue",
expectPartitionTag: "0",
},
{
name: "worker-commands",
partition: newTestTaskQueue(ns.ID().String(), "/temporal-sys/worker-commands/ns/key", enumspb.TASK_QUEUE_TYPE_NEXUS).WorkerCommandsPartition(),
expectTQValue: "/temporal-sys/worker-commands/ns/key",
expectPartitionTag: "__worker_commands__",
},
{
name: "sticky",
partition: newTestTaskQueue(ns.ID().String(), "my-task-queue", enumspb.TASK_QUEUE_TYPE_WORKFLOW).StickyPartition(uuid.NewString()),
expectTQValue: "my-task-queue",
expectPartitionTag: "__sticky__",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
capture := captureHandler.StartCapture()
tqConfig := newTaskQueueConfig(tc.partition.TaskQueue(), config, matchingTestNamespace)
_, _, handler := e.loggerAndMetricsForPartition(ns, tc.partition, tqConfig)
metrics.PollSuccessPerTaskQueueCounter.With(handler).Record(1)
snap := capture.Snapshot()
captureHandler.StopCapture(capture)
recordings := snap["poll_success"]
require.NotEmpty(t, recordings, "expected poll_success metric to be recorded")
found := false
for _, rec := range recordings {
if rec.Tags["taskqueue"] == tc.expectTQValue && rec.Tags["partition"] == tc.expectPartitionTag {
found = true
}
}
require.True(t, found, "expected taskqueue=%q partition=%q, got: %v", tc.expectTQValue, tc.expectPartitionTag, recordings)
})
}
}
// TestLoggerAndMetricsForPartition_BreakdownDisabled verifies the taskqueue and partition metric
// tags for each task queue kind with BreakdownMetricsByTaskQueue=false.
func TestLoggerAndMetricsForPartition_BreakdownDisabled(t *testing.T) {
t.Parallel()
controller := gomock.NewController(t)
ns, mockNamespaceCache := createMockNamespaceCache(controller, matchingTestNamespace)
dc := dynamicconfig.StaticClient{
dynamicconfig.MetricsBreakdownByTaskQueue.Key(): false,
}
config := NewConfig(dynamicconfig.NewCollection(dc, log.NewNoopLogger()))
config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(100 * time.Millisecond)
e := createTestMatchingEngine(log.NewTestLogger(), controller, config, nil, mockNamespaceCache)
captureHandler := metricstest.NewCaptureHandler()
e.metricsHandler = captureHandler
tests := []struct {
name string
partition tqid.Partition
expectTQValue string
expectPartitionTag string
}{
{
name: "normal",
partition: newRootPartition(ns.ID().String(), "my-task-queue", enumspb.TASK_QUEUE_TYPE_NEXUS),
expectTQValue: "__omitted__",
expectPartitionTag: "0",
},
{
name: "sticky",
partition: newTestTaskQueue(ns.ID().String(), "my-task-queue", enumspb.TASK_QUEUE_TYPE_WORKFLOW).StickyPartition(uuid.NewString()),
expectTQValue: "__omitted__",
expectPartitionTag: "__sticky__",
},
{
name: "worker-commands",
partition: newTestTaskQueue(ns.ID().String(), "/temporal-sys/worker-commands/ns/key", enumspb.TASK_QUEUE_TYPE_NEXUS).WorkerCommandsPartition(),
expectTQValue: "__omitted__",
expectPartitionTag: "__worker_commands__",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
capture := captureHandler.StartCapture()
tqConfig := newTaskQueueConfig(tc.partition.TaskQueue(), config, matchingTestNamespace)
_, _, handler := e.loggerAndMetricsForPartition(ns, tc.partition, tqConfig)
metrics.PollSuccessPerTaskQueueCounter.With(handler).Record(1)
snap := capture.Snapshot()
captureHandler.StopCapture(capture)
recordings := snap["poll_success"]
require.NotEmpty(t, recordings, "expected poll_success metric to be recorded")
found := false
for _, rec := range recordings {
if rec.Tags["taskqueue"] == tc.expectTQValue && rec.Tags["partition"] == tc.expectPartitionTag {
found = true
}
}
require.True(t, found, "expected taskqueue=%q partition=%q, got: %v", tc.expectTQValue, tc.expectPartitionTag, recordings)
})
}
}
func TestConvertPollWorkflowTaskQueueResponse(t *testing.T) {
t.Parallel()
tests := []struct {
name string
inputResp *matchingservice.PollWorkflowTaskQueueResponse
expectHistory bool
expectSearchAttributeProcessing bool
}{
{
name: "nil response returns nil",
inputResp: nil,
},
{
name: "response with History field only",
// History field means it's already processed by history service
inputResp: &matchingservice.PollWorkflowTaskQueueResponse{
TaskToken: []byte("token"),
History: &historypb.History{
Events: []*historypb.HistoryEvent{
{
EventId: 1,
EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED,
Attributes: &historypb.HistoryEvent_WorkflowExecutionStartedEventAttributes{
WorkflowExecutionStartedEventAttributes: &historypb.WorkflowExecutionStartedEventAttributes{
SearchAttributes: &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{
"CustomKeywordField": {Data: []byte("test")},
},
},
},
},
},
},
},
},
expectHistory: true,
expectSearchAttributeProcessing: false, // History field = already processed
},
{
name: "response with RawHistory field only",
// RawHistory field means it came from raw bytes and needs processing
inputResp: &matchingservice.PollWorkflowTaskQueueResponse{
TaskToken: []byte("token"),
RawHistory: &historypb.History{
Events: []*historypb.HistoryEvent{
{
EventId: 1,
EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED,
Attributes: &historypb.HistoryEvent_WorkflowExecutionStartedEventAttributes{
WorkflowExecutionStartedEventAttributes: &historypb.WorkflowExecutionStartedEventAttributes{
SearchAttributes: &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{
"CustomKeywordField": {Data: []byte("test")},
},
},
},
},
},
},
},
},
expectHistory: true,
expectSearchAttributeProcessing: true, // RawHistory field = needs processing
},
{
name: "response with both History and RawHistory - History takes precedence",
// When both are present, History takes precedence and it's already processed
inputResp: &matchingservice.PollWorkflowTaskQueueResponse{
TaskToken: []byte("token"),
History: &historypb.History{
Events: []*historypb.HistoryEvent{
{
EventId: 1,
EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED,
Attributes: &historypb.HistoryEvent_WorkflowExecutionStartedEventAttributes{
WorkflowExecutionStartedEventAttributes: &historypb.WorkflowExecutionStartedEventAttributes{
SearchAttributes: &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{
"CustomKeywordField": {Data: []byte("test")},
},
},
},
},
},
},
},
RawHistory: &historypb.History{
Events: []*historypb.HistoryEvent{
{EventId: 2, EventType: enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED},
},
},
},
expectHistory: true,
expectSearchAttributeProcessing: false, // History field takes precedence = already processed
},
{
name: "response with no history",
inputResp: &matchingservice.PollWorkflowTaskQueueResponse{
TaskToken: []byte("token"),
},
expectHistory: false,
expectSearchAttributeProcessing: false,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSAProvider := searchattribute.NewMockProvider(ctrl)
mockSAMapperProvider := searchattribute.NewMockMapperProvider(ctrl)
mockVisibilityManager := manager.NewMockVisibilityManager(ctrl)
engine := &matchingEngineImpl{
config: defaultTestConfig(),
saProvider: mockSAProvider,
saMapperProvider: mockSAMapperProvider,
visibilityManager: mockVisibilityManager,
}
// Set up expectations for search attribute processing.
// Using Times(1) to verify ProcessOutgoingSearchAttributes is actually called.
if tc.expectSearchAttributeProcessing && tc.inputResp != nil {
mockVisibilityManager.EXPECT().GetIndexName().Return("test-index").Times(1)
mockSAProvider.EXPECT().GetSearchAttributes("test-index", false).Return(searchattribute.NameTypeMap{}, nil).Times(1)
mockSAMapperProvider.EXPECT().GetMapper(gomock.Any()).Return(nil, nil).Times(1)
}
result, err := engine.convertPollWorkflowTaskQueueResponse(tc.inputResp, "test-namespace")
require.NoError(t, err)
if tc.inputResp == nil {
assert.Nil(t, result)
return
}
require.NotNil(t, result)
assert.Equal(t, tc.inputResp.TaskToken, result.TaskToken)
if tc.expectHistory {
assert.NotNil(t, result.History)
// When both History and RawHistory are present, History takes precedence
if tc.inputResp.History != nil {
assert.Equal(t, tc.inputResp.History.Events[0].EventId, result.History.Events[0].EventId)
} else if tc.inputResp.RawHistory != nil {
assert.Equal(t, tc.inputResp.RawHistory.Events[0].EventId, result.History.Events[0].EventId)
}
} else {
assert.Nil(t, result.History)
}
})
}
}
func TestConvertPollWorkflowTaskQueueResponse_FieldMapping(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSAProvider := searchattribute.NewMockProvider(ctrl)
mockSAMapperProvider := searchattribute.NewMockMapperProvider(ctrl)
mockVisibilityManager := manager.NewMockVisibilityManager(ctrl)
engine := &matchingEngineImpl{
config: defaultTestConfig(),
saProvider: mockSAProvider,
saMapperProvider: mockSAMapperProvider,
visibilityManager: mockVisibilityManager,
}
inputResp := &matchingservice.PollWorkflowTaskQueueResponse{
TaskToken: []byte("token"),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: "wf-id",
RunId: "run-id",
},
WorkflowType: &commonpb.WorkflowType{
Name: "test-workflow",
},
PreviousStartedEventId: 5,
StartedEventId: 10,
Attempt: 2,
NextEventId: 15,
BacklogCountHint: 100,
StickyExecutionEnabled: true,
Query: &querypb.WorkflowQuery{
QueryType: "test-query",
},
BranchToken: []byte("branch-token"),
NextPageToken: []byte("next-page-token"),
History: &historypb.History{
Events: []*historypb.HistoryEvent{
{EventId: 1, EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED},
},
},
}
result, err := engine.convertPollWorkflowTaskQueueResponse(inputResp, "test-namespace")
require.NoError(t, err)
require.NotNil(t, result)
// Verify all fields are correctly mapped
assert.Equal(t, inputResp.TaskToken, result.TaskToken)
assert.Equal(t, inputResp.WorkflowExecution.WorkflowId, result.WorkflowExecution.WorkflowId)
assert.Equal(t, inputResp.WorkflowExecution.RunId, result.WorkflowExecution.RunId)
assert.Equal(t, inputResp.WorkflowType.Name, result.WorkflowType.Name)
assert.Equal(t, inputResp.PreviousStartedEventId, result.PreviousStartedEventId)
assert.Equal(t, inputResp.StartedEventId, result.StartedEventId)
assert.Equal(t, inputResp.Attempt, result.Attempt)
assert.Equal(t, inputResp.NextEventId, result.NextEventId)
assert.Equal(t, inputResp.BacklogCountHint, result.BacklogCountHint)
assert.Equal(t, inputResp.StickyExecutionEnabled, result.StickyExecutionEnabled)
assert.Equal(t, inputResp.Query.QueryType, result.Query.QueryType)
assert.Equal(t, inputResp.BranchToken, result.BranchToken)
assert.Equal(t, inputResp.NextPageToken, result.NextPageToken)
assert.Equal(t, inputResp.History.Events[0].EventId, result.History.Events[0].EventId)
}
func TestGetHistoryForQueryTask(t *testing.T) {
t.Parallel()
nsID := namespace.ID("test-namespace-id")
nsName := namespace.Name("test-namespace")
workflowID := "test-workflow-id"
runID := "test-run-id"
tests := []struct {
name string
isStickyEnabled bool
setupMocks func(*historyservicemock.MockHistoryServiceClient, *namespace.MockRegistry)
expectHistory bool
expectNextPageToken bool
expectError bool
}{
{
name: "sticky enabled returns empty history",
isStickyEnabled: true,
setupMocks: func(_ *historyservicemock.MockHistoryServiceClient, _ *namespace.MockRegistry) {},
expectHistory: true, // empty history
},
{
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(
&historyservice.GetWorkflowExecutionHistoryResponse{
Response: &workflowservice.GetWorkflowExecutionHistoryResponse{
History: &historypb.History{
Events: []*historypb.HistoryEvent{
{EventId: 1, EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED},
},
},
NextPageToken: []byte("next-token"),
},
}, nil).Times(1)
},
expectHistory: true,
expectNextPageToken: true,
},
{
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)
},
expectError: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(ctrl)
mockNsRegistry := namespace.NewMockRegistry(ctrl)
mockSAProvider := searchattribute.NewMockProvider(ctrl)
mockSAMapperProvider := searchattribute.NewMockMapperProvider(ctrl)
mockVisibilityManager := manager.NewMockVisibilityManager(ctrl)
config := defaultTestConfig()
// 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()
}
engine := &matchingEngineImpl{
config: config,
historyClient: mockHistoryClient,
namespaceRegistry: mockNsRegistry,
saProvider: mockSAProvider,
saMapperProvider: mockSAMapperProvider,
visibilityManager: mockVisibilityManager,
logger: log.NewNoopLogger(),
}
tc.setupMocks(mockHistoryClient, mockNsRegistry)
// Create a mock task
task := &internalTask{
namespace: nsName,
event: &genericTaskInfo{
AllocatedTaskInfo: &persistencespb.AllocatedTaskInfo{
Data: &persistencespb.TaskInfo{
NamespaceId: nsID.String(),
WorkflowId: workflowID,
RunId: runID,
},
},
},
}
hist, nextPageToken, err := engine.getHistoryForQueryTask(
context.Background(),
nsID,
task,
tc.isStickyEnabled,
)
if tc.expectError {
assert.Error(t, err)
return
}
require.NoError(t, err)
if tc.isStickyEnabled {
assert.NotNil(t, hist)
assert.Empty(t, hist.Events)
assert.Nil(t, nextPageToken)
return
}
if tc.expectHistory {
assert.NotNil(t, hist)
}
if tc.expectNextPageToken {
assert.NotNil(t, nextPageToken)
}
})
}
}
func validateTimeRange(t time.Time, expectedDuration time.Duration) bool {
currentTime := time.Now().UTC()
diff := time.Duration(currentTime.UnixNano() - t.UnixNano())
if diff > expectedDuration {
fmt.Printf("Current time: %v, Application time: %v, Difference: %v \n", currentTime, t, diff)
return false
}
return true
}
func defaultTestConfig() *Config {
config := NewConfig(dynamicconfig.NewNoopCollection())
config.LongPollExpirationInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(100 * time.Millisecond)
config.MaxTaskDeleteBatchSize = dynamicconfig.GetIntPropertyFnFilteredByTaskQueue(1)
config.AutoEnableV2Sub = trueTaskQueueSub
// Always update metadata on append in tests so backlog count assertions are exact.
config.MetadataUpdateOnAppendInterval = dynamicconfig.GetDurationPropertyFnFilteredByTaskQueue(0)
return config
}
type (
dynamicRateBurstWrapper struct {
quotas.MutableRateBurst
*quotas.RateLimiterImpl
}
)
func (d *dynamicRateBurstWrapper) SetRPS(rps float64) {
d.MutableRateBurst.SetRPS(rps)
d.RateLimiterImpl.SetRPS(rps)
}
func (d *dynamicRateBurstWrapper) SetBurst(burst int) {
d.MutableRateBurst.SetBurst(burst)
d.RateLimiterImpl.SetBurst(burst)
}
func (d *dynamicRateBurstWrapper) Rate() float64 {
return d.RateLimiterImpl.Rate()
}
func (d *dynamicRateBurstWrapper) Burst() int {
return d.RateLimiterImpl.Burst()
}
// TODO(pri): cleanup; delete this
func useClassicMatcher(config *Config) {
config.NewMatcherSub = staticFalseChange
}
func useFairness(config *Config) {
config.EnableFairnessSub = staticTrueChange
}
func staticTrueChange(_, _ string, _ enumspb.TaskQueueType, _ func(dynamicconfig.GradualChange[bool])) (dynamicconfig.GradualChange[bool], func()) {
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() {}
}
func TestCancelOutstandingWorkerPolls(t *testing.T) {
t.Parallel()
t.Run("unknown worker key succeeds", 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}),
}
resp, err := engine.CancelOutstandingWorkerPolls(context.Background(),
&matchingservice.CancelOutstandingWorkerPollsRequest{
WorkerInstanceKey: "unknown-worker",
})
require.NoError(t, err)
require.Equal(t, int32(0), resp.CancelledCount)
})
t.Run("cancels all polls for worker", 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"
cancelledCount := 0
// Set up 3 pollers for this worker
engine.workerInstancePollers.Add(workerKey, "poller-0", func() { cancelledCount++ })
engine.workerInstancePollers.Add(workerKey, "poller-1", func() { cancelledCount++ })
engine.workerInstancePollers.Add(workerKey, "poller-2", func() { cancelledCount++ })
resp, err := engine.CancelOutstandingWorkerPolls(context.Background(),
&matchingservice.CancelOutstandingWorkerPollsRequest{
WorkerInstanceKey: workerKey,
})
require.NoError(t, err)
require.Equal(t, int32(3), resp.CancelledCount)
require.Equal(t, 3, cancelledCount)
})
t.Run("does not affect other workers", func(t *testing.T) {
t.Parallel()
worker1Cancelled := false
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
engine.workerInstancePollers.Add("worker-1", "poller-1", func() { worker1Cancelled = true })
engine.workerInstancePollers.Add("worker-2", "poller-2", func() { worker2Cancelled = true })
// Cancel worker1's polls only
resp, err := engine.CancelOutstandingWorkerPolls(context.Background(),
&matchingservice.CancelOutstandingWorkerPollsRequest{
WorkerInstanceKey: "worker-1",
})
require.NoError(t, err)
require.Equal(t, int32(1), resp.CancelledCount)
require.True(t, worker1Cancelled)
require.False(t, worker2Cancelled)
})
t.Run("cancels forwarded polls with same pollerID on different partitions", 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"
pollerID := "same-poller-id"
childCancelled := false
parentCancelled := false
// Simulate forwarding: same pollerID registered on child and parent partitions.
// The key includes partition name to prevent parent from overwriting child.
engine.workerInstancePollers.Add(workerKey, "/_sys/test-queue/1:"+pollerID, func() { childCancelled = true })
engine.workerInstancePollers.Add(workerKey, "test-queue:"+pollerID, func() { parentCancelled = true })
resp, err := engine.CancelOutstandingWorkerPolls(context.Background(),
&matchingservice.CancelOutstandingWorkerPollsRequest{
WorkerInstanceKey: workerKey,
})
require.NoError(t, err)
require.Equal(t, int32(2), resp.CancelledCount)
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())
})
}
// 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")
}