Files
tasks/service/matching/physical_task_queue_manager.go
David ReissandCarly de Frondeville 0078fa0f51 Consolidate default priority handling (#8483)
## What changed?
- Set default priority on all internalTasks after creation.
- Combine effective priority and poll forwarder flag into one value.
- For simplicity, default priority and level count are set at partition
load time.

## Why?
Tasks with unset priority key were not actually getting the default
value when read from backlog. This sets the default priority on all
sync, backlog, query, and nexus tasks. (Priority is not meaningful for
sync match tasks, and nexus has no priority yet, but they're set anyway
for consistency.) This will also make it easier to adjust the priority
for other purposes, e.g. prioritizing the old backlog for migrating
persistence.

## How did you test it?
- [x] built
- [x] run locally and tested manually
- [x] covered by existing tests
- [ ] added new unit test(s)
- [x] modified functional test
2025-10-17 14:22:19 -07:00

815 lines
29 KiB
Go

package matching
import (
"context"
"errors"
"fmt"
"math/rand/v2"
"sync"
"sync/atomic"
"time"
"github.com/nexus-rpc/sdk-go/nexus"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
taskqueuepb "go.temporal.io/api/taskqueue/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/server/api/matchingservice/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
taskqueuespb "go.temporal.io/server/api/taskqueue/v1"
"go.temporal.io/server/common"
"go.temporal.io/server/common/clock"
"go.temporal.io/server/common/cluster"
"go.temporal.io/server/common/contextutil"
"go.temporal.io/server/common/debug"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/common/quotas"
"go.temporal.io/server/common/testing/testhooks"
"go.temporal.io/server/common/worker_versioning"
"go.temporal.io/server/service/matching/counter"
"go.temporal.io/server/service/worker/workerdeployment"
"google.golang.org/protobuf/types/known/durationpb"
)
const (
// Time budget for empty task to propagate through the function stack and be returned to
// pollForActivityTask or pollForWorkflowTask handler.
returnEmptyTaskTimeBudget = time.Second
// Fake Task ID to wrap a task for syncmatch
syncMatchTaskId = -137
ioTimeout = 5 * time.Second * debug.TimeoutMultiplier
// Threshold for counting a AddTask call as a no recent poller call
noPollerThreshold = time.Minute * 2
// We avoid retrying failed deployment registration for this period.
deploymentRegisterErrorBackoff = 5 * time.Second
)
type (
addTaskParams struct {
taskInfo *persistencespb.TaskInfo
forwardInfo *taskqueuespb.TaskForwardInfo
}
// physicalTaskQueueManagerImpl manages a set of physical queues that comprise one logical
// queue, corresponding to a single versioned queue of a task queue partition.
// TODO(pri): rename this
physicalTaskQueueManagerImpl struct {
status int32
partitionMgr *taskQueuePartitionManagerImpl
queue *PhysicalTaskQueueKey
config *taskQueueConfig
// This context is valid for lifetime of this physicalTaskQueueManagerImpl.
// It can be used to notify when the task queue is closing.
tqCtx context.Context
tqCtxCancel context.CancelFunc
cancelMatcherSub func()
cancelFairnessSub func()
backlogMgr backlogManager
liveness *liveness
oldMatcher *TaskMatcher // TODO(pri): old matcher cleanup
priMatcher *priTaskMatcher
matcher matcherInterface // TODO(pri): old matcher cleanup
namespaceRegistry namespace.Registry
logger log.Logger
throttledLogger log.ThrottledLogger
matchingClient matchingservice.MatchingServiceClient
clusterMeta cluster.Metadata
metricsHandler metrics.Handler // namespace/taskqueue tagged metric scope
// pollerHistory stores poller which poll from this taskqueue in last few minutes
pollerHistory *pollerHistory
currentPolls atomic.Int64
taskValidator taskValidator
deploymentRegistrationCh chan struct{}
deploymentVersionRegistered bool
pollerScalingRateLimiter quotas.RateLimiter
taskTrackerLock sync.RWMutex
tasksAdded map[priorityKey]*taskTracker
tasksDispatched map[priorityKey]*taskTracker
}
// TODO(pri): old matcher cleanup
matcherInterface interface {
Start()
Stop()
Poll(ctx context.Context, pollMetadata *pollMetadata) (*internalTask, error)
PollForQuery(ctx context.Context, pollMetadata *pollMetadata) (*internalTask, error)
OfferQuery(ctx context.Context, task *internalTask) (*matchingservice.QueryWorkflowResponse, error)
OfferNexusTask(ctx context.Context, task *internalTask) (*matchingservice.DispatchNexusTaskResponse, error)
ReprocessAllTasks()
}
)
var _ physicalTaskQueueManager = (*physicalTaskQueueManagerImpl)(nil)
var (
errRemoteSyncMatchFailed = serviceerror.NewCanceled("remote sync match failed")
errMissingNormalQueueName = errors.New("missing normal queue name")
errDeploymentVersionNotReady = serviceerror.NewUnavailable("task queue is not ready to process polls from this deployment version, try again shortly")
ErrBlackholedQuery = "You are trying to query a closed Workflow that is PINNED to Worker Deployment Version %s, but %s is drained and has no pollers to answer the query. Immediately: You can re-deploy Workers in this Deployment Version to take those queries, or you can workflow update-options to change your workflow to AUTO_UPGRADE. For the future: In your infrastructure, consider waiting longer after the last queried timestamp as reported in Describe Deployment before you sunset Workers. Or mark this workflow as AUTO_UPGRADE."
backlogTagClassic = tag.NewStringTag("backlog", "classic")
backlogTagPriority = tag.NewStringTag("backlog", "priority")
backlogTagFairness = tag.NewStringTag("backlog", "fairness")
)
func newPhysicalTaskQueueManager(
partitionMgr *taskQueuePartitionManagerImpl,
queue *PhysicalTaskQueueKey,
) (*physicalTaskQueueManagerImpl, error) {
e := partitionMgr.engine
config := partitionMgr.config
buildIdTagValue := queue.Version().MetricsTagValue()
buildIdTag := tag.WorkerBuildId(buildIdTagValue)
taggedMetricsHandler := partitionMgr.metricsHandler.WithTags(
metrics.OperationTag(metrics.MatchingTaskQueueMgrScope),
metrics.WorkerBuildIdTag(buildIdTagValue, config.BreakdownMetricsByBuildID()))
tqCtx, tqCancel := context.WithCancel(partitionMgr.callerInfoContext(context.Background()))
// We multiply by a big number so that we can later divide it by the number of pollers when grabbing permits,
// to allow us to make more decisions per second when there are more pollers.
pollerScalingRateLimitFn := func() float64 {
return config.PollerScalingDecisionsPerSecond() * 1e6
}
pqMgr := &physicalTaskQueueManagerImpl{
status: common.DaemonStatusInitialized,
partitionMgr: partitionMgr,
queue: queue,
config: config,
tqCtx: tqCtx,
tqCtxCancel: tqCancel,
namespaceRegistry: e.namespaceRegistry,
matchingClient: e.matchingRawClient,
clusterMeta: e.clusterMeta,
metricsHandler: taggedMetricsHandler,
tasksAdded: make(map[priorityKey]*taskTracker),
tasksDispatched: make(map[priorityKey]*taskTracker),
pollerScalingRateLimiter: quotas.NewDefaultOutgoingRateLimiter(pollerScalingRateLimitFn),
deploymentRegistrationCh: make(chan struct{}, 1),
}
pqMgr.deploymentRegistrationCh <- struct{}{} // seed
pqMgr.pollerHistory = newPollerHistory(partitionMgr.config.PollerHistoryTTL())
pqMgr.liveness = newLiveness(
clock.NewRealTimeSource(),
config.MaxTaskQueueIdleTime,
func() { pqMgr.UnloadFromPartitionManager(unloadCauseIdle) },
)
pqMgr.taskValidator = newTaskValidator(
tqCtx,
pqMgr.clusterMeta,
pqMgr.namespaceRegistry,
pqMgr.partitionMgr.engine.historyClient,
)
isSticky := queue.Partition().Kind() == enumspb.TASK_QUEUE_KIND_STICKY
isChild := !isSticky && !queue.Partition().IsRoot()
var fairness, newMatcher bool
// Fairness is disabled for sticky queues for now so that we can still use TTLs.
if !isSticky {
fairness, pqMgr.cancelFairnessSub = config.EnableFairness(func(bool) {
// unload on change so that we can reload with the new setting:
pqMgr.UnloadFromPartitionManager(unloadCauseConfigChange)
})
}
if fairness {
pqMgr.logger = log.With(partitionMgr.logger, buildIdTag, backlogTagFairness)
pqMgr.throttledLogger = log.With(partitionMgr.throttledLogger, buildIdTag, backlogTagFairness)
counterFactory := func() counter.Counter {
src := rand.NewPCG(rand.Uint64(), rand.Uint64())
return counter.NewHybridCounter(config.FairnessCounter(), src)
}
pqMgr.backlogMgr = newFairBacklogManager(
tqCtx,
pqMgr,
config,
e.fairTaskManager,
pqMgr.logger,
pqMgr.throttledLogger,
e.matchingRawClient,
newFairMetricsHandler(taggedMetricsHandler),
counterFactory,
)
var fwdr *priForwarder
var err error
if isChild {
// Every DB Queue needs its own forwarder so that the throttles do not interfere
fwdr, err = newPriForwarder(&config.forwarderConfig, queue, e.matchingRawClient)
if err != nil {
return nil, err
}
}
pqMgr.priMatcher = newPriTaskMatcher(
tqCtx,
config,
queue.partition,
fwdr,
pqMgr.taskValidator,
pqMgr.logger,
newFairMetricsHandler(taggedMetricsHandler),
partitionMgr.rateLimitManager,
pqMgr.MarkAlive,
)
pqMgr.matcher = pqMgr.priMatcher
return pqMgr, nil
}
newMatcher, pqMgr.cancelMatcherSub = config.NewMatcher(func(bool) {
// unload on change to NewMatcher so that we can reload with the new setting:
pqMgr.UnloadFromPartitionManager(unloadCauseConfigChange)
})
if newMatcher {
pqMgr.logger = log.With(partitionMgr.logger, buildIdTag, backlogTagPriority)
pqMgr.throttledLogger = log.With(partitionMgr.throttledLogger, buildIdTag, backlogTagPriority)
pqMgr.backlogMgr = newPriBacklogManager(
tqCtx,
pqMgr,
config,
e.taskManager,
pqMgr.logger,
pqMgr.throttledLogger,
e.matchingRawClient,
newPriMetricsHandler(taggedMetricsHandler),
)
var fwdr *priForwarder
var err error
if isChild {
// Every DB Queue needs its own forwarder so that the throttles do not interfere
fwdr, err = newPriForwarder(&config.forwarderConfig, queue, e.matchingRawClient)
if err != nil {
return nil, err
}
}
pqMgr.priMatcher = newPriTaskMatcher(
tqCtx,
config,
queue.partition,
fwdr,
pqMgr.taskValidator,
pqMgr.logger,
newPriMetricsHandler(taggedMetricsHandler),
partitionMgr.rateLimitManager,
pqMgr.MarkAlive,
)
pqMgr.matcher = pqMgr.priMatcher
return pqMgr, nil
}
pqMgr.logger = log.With(partitionMgr.logger, buildIdTag, backlogTagClassic)
pqMgr.throttledLogger = log.With(partitionMgr.throttledLogger, buildIdTag, backlogTagClassic)
pqMgr.backlogMgr = newBacklogManager(
tqCtx,
pqMgr,
config,
e.taskManager,
pqMgr.logger,
pqMgr.throttledLogger,
e.matchingRawClient,
taggedMetricsHandler,
)
var fwdr *Forwarder
var err error
if isChild {
// Every DB Queue needs its own forwarder so that the throttles do not interfere
fwdr, err = newForwarder(&config.forwarderConfig, queue, e.matchingRawClient)
if err != nil {
return nil, err
}
}
pqMgr.oldMatcher = newTaskMatcher(config, fwdr, taggedMetricsHandler, pqMgr.partitionMgr.GetRateLimitManager().GetRateLimiter())
pqMgr.matcher = pqMgr.oldMatcher
return pqMgr, nil
}
func (c *physicalTaskQueueManagerImpl) Start() {
if !atomic.CompareAndSwapInt32(
&c.status,
common.DaemonStatusInitialized,
common.DaemonStatusStarted,
) {
return
}
c.liveness.Start()
c.backlogMgr.Start()
c.matcher.Start()
c.logger.Info("Started physicalTaskQueueManager", tag.LifeCycleStarted, tag.Cause(c.config.loadCause.String()))
c.metricsHandler.Counter(metrics.TaskQueueStartedCounter.Name()).Record(1)
c.partitionMgr.engine.updatePhysicalTaskQueueGauge(c.partitionMgr.ns, c.partitionMgr.partition, c.queue.version, 1)
}
// Stop does not unload the queue from its partition. It is intended to be called by the partition manager when
// unloading a queues. For stopping and unloading a queue call UnloadFromPartitionManager instead.
func (c *physicalTaskQueueManagerImpl) Stop(unloadCause unloadCause) {
if !atomic.CompareAndSwapInt32(
&c.status,
common.DaemonStatusStarted,
common.DaemonStatusStopped,
) {
return
}
if c.cancelMatcherSub != nil {
c.cancelMatcherSub()
}
if c.cancelFairnessSub != nil {
c.cancelFairnessSub()
}
// this may attempt to write one final ack update, do this before canceling tqCtx
c.backlogMgr.Stop()
c.matcher.Stop()
c.liveness.Stop()
c.tqCtxCancel()
c.logger.Info("Stopped physicalTaskQueueManager", tag.LifeCycleStopped, tag.Cause(unloadCause.String()))
c.metricsHandler.Counter(metrics.TaskQueueStoppedCounter.Name()).Record(1)
c.partitionMgr.engine.updatePhysicalTaskQueueGauge(c.partitionMgr.ns, c.partitionMgr.partition, c.queue.version, -1)
}
func (c *physicalTaskQueueManagerImpl) WaitUntilInitialized(ctx context.Context) error {
return c.backlogMgr.WaitUntilInitialized(ctx)
}
func (c *physicalTaskQueueManagerImpl) SpoolTask(taskInfo *persistencespb.TaskInfo) error {
c.liveness.markAlive()
return c.backlogMgr.SpoolTask(taskInfo)
}
// PollTask blocks waiting for a task.
// Returns error when context deadline is exceeded
// maxDispatchPerSecond is the max rate at which tasks are allowed
// to be dispatched from this task queue to pollers
func (c *physicalTaskQueueManagerImpl) PollTask(
ctx context.Context,
pollMetadata *pollMetadata,
) (*internalTask, error) {
c.liveness.markAlive()
c.currentPolls.Add(1)
defer c.currentPolls.Add(-1)
namespaceId := namespace.ID(c.queue.NamespaceId())
namespaceEntry, err := c.namespaceRegistry.GetNamespaceByID(namespaceId)
if err != nil {
return nil, err
}
if c.partitionMgr.engine.config.EnableDeploymentVersions(namespaceEntry.Name().String()) {
if err = c.ensureRegisteredInDeploymentVersion(ctx, namespaceEntry, pollMetadata); err != nil {
return nil, err
}
}
if !namespaceEntry.ActiveInCluster(c.clusterMeta.GetCurrentClusterName()) {
return c.matcher.PollForQuery(ctx, pollMetadata)
}
for {
task, err := c.matcher.Poll(ctx, pollMetadata)
if err != nil {
return nil, err
}
// It's possible to get an expired task here: taskReader checks for expiration when
// reading from persistence, and physicalTaskQueueManager checks for expiration in
// ProcessSpooledTask, but the one task blocked in the matcher could expire while it's
// there. In that case, go back for another task.
// If we didn't do this, the task would be rejected when we call RecordXTaskStarted on
// history, but this is more efficient.
if task.event != nil && IsTaskExpired(task.event.AllocatedTaskInfo) {
// task is expired while polling
c.metricsHandler.Counter(metrics.ExpiredTasksPerTaskQueueCounter.Name()).Record(1, metrics.TaskExpireStageMemoryTag)
task.finish(nil, false)
continue
}
task.namespace = c.partitionMgr.ns.Name()
task.backlogCountHint = c.backlogCountHint
if pollMetadata.forwardedFrom == "" && // only track the original polls, not forwarded ones.
(!task.isStarted() || !task.started.hasEmptyResponse()) { // Need to filter out the empty "started" ones
c.getOrCreateTaskTracker(c.tasksDispatched, priorityKey(task.getPriority().GetPriorityKey())).incrementTaskCount()
}
return task, nil
}
}
func (c *physicalTaskQueueManagerImpl) backlogCountHint() int64 {
return c.backlogMgr.BacklogCountHint()
}
func (c *physicalTaskQueueManagerImpl) MarkAlive() {
c.liveness.markAlive()
}
// DispatchSpooledTask dispatches a task to a poller. When there are no pollers to pick
// up the task or if rate limit is exceeded, this method will return error. Task
// *will not* be persisted to db
// TODO(pri): old matcher cleanup
func (c *physicalTaskQueueManagerImpl) DispatchSpooledTask(
ctx context.Context,
task *internalTask,
userDataChanged <-chan struct{},
) error {
return c.oldMatcher.MustOffer(ctx, task, userDataChanged)
}
// TODO(pri): old matcher cleanup
func (c *physicalTaskQueueManagerImpl) ProcessSpooledTask(
ctx context.Context,
task *internalTask,
) error {
if !c.taskValidator.maybeValidate(task.event.AllocatedTaskInfo, c.queue.TaskType()) {
task.finish(nil, false)
var invalidTaskTag = getInvalidTaskTag(task)
c.metricsHandler.Counter(metrics.ExpiredTasksPerTaskQueueCounter.Name()).Record(1, invalidTaskTag)
// Don't try to set read level here because it may have been advanced already.
// Stay alive as long as we're invalidating tasks
c.MarkAlive()
return nil
}
return c.partitionMgr.ProcessSpooledTask(ctx, task, c.queue)
}
func (c *physicalTaskQueueManagerImpl) AddSpooledTask(task *internalTask) error {
return c.partitionMgr.AddSpooledTask(c.tqCtx, task, c.queue)
}
func (c *physicalTaskQueueManagerImpl) AddSpooledTaskToMatcher(task *internalTask) {
c.priMatcher.AddTask(task)
}
func (c *physicalTaskQueueManagerImpl) UserDataChanged() {
c.matcher.ReprocessAllTasks()
}
// DispatchQueryTask will dispatch query to local or remote poller. If forwarded then result or error is returned,
// if dispatched to local poller then nil and nil is returned.
func (c *physicalTaskQueueManagerImpl) DispatchQueryTask(
ctx context.Context,
taskId string,
request *matchingservice.QueryWorkflowRequest,
) (*matchingservice.QueryWorkflowResponse, error) {
task := newInternalQueryTask(taskId, request)
c.config.setDefaultPriority(task)
if !task.isForwarded() {
c.getOrCreateTaskTracker(c.tasksAdded, priorityKey(request.GetPriority().GetPriorityKey())).incrementTaskCount()
}
return c.matcher.OfferQuery(ctx, task)
}
func (c *physicalTaskQueueManagerImpl) DispatchNexusTask(
ctx context.Context,
taskId string,
request *matchingservice.DispatchNexusTaskRequest,
) (*matchingservice.DispatchNexusTaskResponse, error) {
deadline, _ := ctx.Deadline() // If not set by user, our client will set a default.
var opDeadline time.Time
if header := nexus.Header(request.GetRequest().GetHeader()); header != nil {
if opTimeoutHeader := header.Get(nexus.HeaderOperationTimeout); opTimeoutHeader != "" {
opTimeout, err := time.ParseDuration(opTimeoutHeader)
if err != nil {
// Operation-Timeout header is not required so don't fail request on parsing errors.
c.logger.Warn(fmt.Sprintf("unable to parse %v header: %v", nexus.HeaderOperationTimeout, opTimeoutHeader), tag.Error(err), tag.WorkflowNamespaceID(request.NamespaceId))
} else {
opDeadline = time.Now().Add(opTimeout)
}
}
}
task := newInternalNexusTask(taskId, deadline, opDeadline, request)
c.config.setDefaultPriority(task)
if !task.isForwarded() {
c.getOrCreateTaskTracker(c.tasksAdded, priorityKey(0)).incrementTaskCount() // Nexus has no priorities
}
return c.matcher.OfferNexusTask(ctx, task)
}
func (c *physicalTaskQueueManagerImpl) UpdatePollerInfo(id pollerIdentity, pollMetadata *pollMetadata) {
c.pollerHistory.updatePollerInfo(id, pollMetadata)
}
// GetAllPollerInfo returns all pollers that polled from this taskqueue in last few minutes
func (c *physicalTaskQueueManagerImpl) GetAllPollerInfo() []*taskqueuepb.PollerInfo {
if c.pollerHistory == nil {
return nil
}
res := c.pollerHistory.getPollerInfo(time.Time{})
return res
}
func (c *physicalTaskQueueManagerImpl) HasPollerAfter(accessTime time.Time) bool {
if c.currentPolls.Load() > 0 {
return true
}
if c.pollerHistory == nil {
return false
}
recentPollers := c.pollerHistory.getPollerInfo(accessTime)
return len(recentPollers) > 0
}
// LegacyDescribeTaskQueue returns information about the target taskqueue, right now this API returns the
// pollers which polled this taskqueue in last few minutes and status of taskqueue's ackManager
// (readLevel, ackLevel, backlogCountHint and taskIDBlock).
func (c *physicalTaskQueueManagerImpl) LegacyDescribeTaskQueue(includeTaskQueueStatus bool) *matchingservice.DescribeTaskQueueResponse {
response := &matchingservice.DescribeTaskQueueResponse{
DescResponse: &workflowservice.DescribeTaskQueueResponse{
Pollers: c.GetAllPollerInfo(),
},
}
if includeTaskQueueStatus {
response.DescResponse.TaskQueueStatus = c.backlogMgr.BacklogStatus()
rps, _ := c.partitionMgr.GetRateLimitManager().GetEffectiveRPSAndSource()
//nolint:staticcheck // SA1019: using deprecated TaskQueueStatus for legacy compatibility
response.DescResponse.TaskQueueStatus.RatePerSecond = rps
}
return response
}
func (c *physicalTaskQueueManagerImpl) GetStatsByPriority() map[int32]*taskqueuepb.TaskQueueStats {
stats := c.backlogMgr.BacklogStatsByPriority()
c.taskTrackerLock.RLock()
defer c.taskTrackerLock.RUnlock()
for pri, tt := range c.tasksAdded {
if _, ok := stats[int32(pri)]; !ok {
stats[int32(pri)] = &taskqueuepb.TaskQueueStats{}
}
stats[int32(pri)].TasksAddRate = tt.rate()
}
for pri, tt := range c.tasksDispatched {
if _, ok := stats[int32(pri)]; !ok {
stats[int32(pri)] = &taskqueuepb.TaskQueueStats{}
}
stats[int32(pri)].TasksDispatchRate = tt.rate()
}
return stats
}
func (c *physicalTaskQueueManagerImpl) GetInternalTaskQueueStatus() []*taskqueuespb.InternalTaskQueueStatus {
return c.backlogMgr.InternalStatus()
}
func (c *physicalTaskQueueManagerImpl) TrySyncMatch(ctx context.Context, task *internalTask) (bool, error) {
if !task.isForwarded() {
// request sent by history service
c.liveness.markAlive()
c.getOrCreateTaskTracker(c.tasksAdded, priorityKey(task.getPriority().GetPriorityKey())).incrementTaskCount()
if disable, _ := testhooks.Get[bool](c.partitionMgr.engine.testHooks, testhooks.MatchingDisableSyncMatch); disable {
return false, nil
}
}
if c.priMatcher != nil {
return c.priMatcher.Offer(ctx, task)
}
childCtx, cancel := contextutil.WithDeadlineBuffer(ctx, c.config.SyncMatchWaitDuration(), time.Second)
defer cancel()
return c.oldMatcher.Offer(childCtx, task)
}
func (c *physicalTaskQueueManagerImpl) ensureRegisteredInDeploymentVersion(
ctx context.Context,
namespaceEntry *namespace.Namespace,
pollMetadata *pollMetadata,
) error {
workerDeployment, err := worker_versioning.DeploymentFromCapabilities(pollMetadata.workerVersionCapabilities, pollMetadata.deploymentOptions)
if err != nil {
return err
}
if workerDeployment == nil {
return nil
}
if !c.partitionMgr.engine.config.EnableDeploymentVersions(namespaceEntry.Name().String()) {
return errMissingDeploymentVersion
}
select {
case <-ctx.Done():
return ctx.Err()
case <-c.deploymentRegistrationCh:
// lock so that only one poll does the update and the rest wait for it
// using a channel instead of mutex so we can honor the context timeout
}
defer func() {
select {
// release the lock
case c.deploymentRegistrationCh <- struct{}{}:
default:
c.logger.Error("deploymentRegistrationCh is already unlocked")
}
}()
if c.deploymentVersionRegistered {
// deployment version already registered
return nil
}
userData, _, err := c.partitionMgr.GetUserDataManager().GetUserData()
if err != nil {
return err
}
deploymentData := userData.GetData().GetPerType()[int32(c.queue.TaskType())].GetDeploymentData()
if worker_versioning.FindDeploymentVersion(deploymentData, worker_versioning.DeploymentVersionFromDeployment(workerDeployment)) != -1 {
// already registered in user data, we can assume the workflow is running.
// TODO: consider replication scenarios where user data is replicated before
// the deployment workflow.
return nil
}
// we need to update the deployment workflow to tell it about this task queue
// TODO: add some backoff here if we got an error last time
err = c.partitionMgr.engine.workerDeploymentClient.RegisterTaskQueueWorker(
ctx, namespaceEntry, workerDeployment.SeriesName, workerDeployment.BuildId, c.queue.TaskQueueFamily().Name(), c.queue.TaskType(),
"matching service")
if err != nil {
if common.IsContextDeadlineExceededErr(err) || common.IsContextCanceledErr(err) {
// error is not from registration, just return it without waiting
return err
}
var errMaxTaskQueuesInVersion workerdeployment.ErrMaxTaskQueuesInVersion
var errMaxVersionsInDeployment workerdeployment.ErrMaxVersionsInDeployment
var errMaxDeploymentsInNamespace workerdeployment.ErrMaxDeploymentsInNamespace
if errors.As(err, &errMaxTaskQueuesInVersion) {
err = errMaxTaskQueuesInVersion
} else if errors.As(err, &errMaxVersionsInDeployment) {
err = errMaxVersionsInDeployment
} else if errors.As(err, &errMaxDeploymentsInNamespace) {
err = errMaxDeploymentsInNamespace
} else {
// Do not surface low level error to user
c.logger.Error("error while registering version", tag.Error(err))
err = errDeploymentVersionNotReady
}
// Before retrying the error, hold the poller for some time so it does not retry immediately
// Parallel polls are already serialized using the lock.
time.Sleep(deploymentRegisterErrorBackoff)
return err
}
// the deployment workflow will register itself in this task queue's user data.
// wait for it to propagate here.
for {
userData, userDataChanged, err := c.partitionMgr.GetUserDataManager().GetUserData()
if err != nil {
return err
}
deploymentData := userData.GetData().GetPerType()[int32(c.queue.TaskType())].GetDeploymentData()
if worker_versioning.FindDeploymentVersion(deploymentData, worker_versioning.DeploymentVersionFromDeployment(workerDeployment)) >= 0 {
break
}
select {
case <-userDataChanged:
case <-ctx.Done():
c.logger.Error("timed out waiting for worker deployment version to appear in user data")
return ctx.Err()
}
}
c.deploymentVersionRegistered = true
return nil
}
func (c *physicalTaskQueueManagerImpl) QueueKey() *PhysicalTaskQueueKey {
return c.queue
}
func (c *physicalTaskQueueManagerImpl) UnloadFromPartitionManager(unloadCause unloadCause) {
c.partitionMgr.unloadPhysicalQueue(c, unloadCause)
}
func (c *physicalTaskQueueManagerImpl) MakePollerScalingDecision(
pollStartTime time.Time) *taskqueuepb.PollerScalingDecision {
return c.makePollerScalingDecisionImpl(pollStartTime, func() *taskqueuepb.TaskQueueStats {
return aggregateStats(c.GetStatsByPriority())
})
}
func (c *physicalTaskQueueManagerImpl) makePollerScalingDecisionImpl(
pollStartTime time.Time,
statsFn func() *taskqueuepb.TaskQueueStats,
) *taskqueuepb.PollerScalingDecision {
pollWaitTime := c.partitionMgr.engine.timeSource.Since(pollStartTime)
// If a poller has waited around a while, we can always suggest a decrease.
if pollWaitTime >= c.partitionMgr.config.PollerScalingWaitTime() {
// Decrease if any poll matched after sitting idle for some configured period
return &taskqueuepb.PollerScalingDecision{
PollRequestDeltaSuggestion: -1,
}
}
// Avoid spiking pollers crazy fast by limiting how frequently change decisions are issued. Be more permissive when
// there are more recent pollers.
numPollers := c.pollerHistory.history.Size()
if numPollers == 0 {
numPollers = 1
}
if !c.pollerScalingRateLimiter.AllowN(time.Now(), 1e6/numPollers) {
return nil
}
delta := int32(0)
stats := statsFn()
if stats.ApproximateBacklogCount > 0 &&
stats.ApproximateBacklogAge.AsDuration() > c.partitionMgr.config.PollerScalingBacklogAgeScaleUp() {
// Always increase when there is a backlog, even if we're a partition. It's also important to increase for
// sticky queues.
delta = 1
} else if !c.queue.Partition().IsRoot() {
// Non-root partitions don't have an appropriate view of the data to make decisions beyond backlog.
return nil
} else if (stats.TasksAddRate / stats.TasksDispatchRate) > 1.2 {
// Increase if we're adding tasks faster than we're dispatching them. Particularly useful for Nexus tasks,
// since those (currently) don't get backlogged.
delta = 1
}
if delta == 0 {
return nil
}
return &taskqueuepb.PollerScalingDecision{
PollRequestDeltaSuggestion: delta,
}
}
func (c *physicalTaskQueueManagerImpl) getOrCreateTaskTracker(
intervals map[priorityKey]*taskTracker,
priorityKey priorityKey,
) *taskTracker {
// priorityKey could be zero here if we're tracking dispatched tasks (i.e. called from PollTask)
// and the poll was forwarded so we have a "started" task. We don't return the priority with the
// started task info so it's not available here. Use the default priority to avoid confusion
// even though it may not be accurate.
// TODO: either return priority with the started task, or do this tracking on the node where the
// match happened, so we have the right value here.
if priorityKey == 0 {
priorityKey = c.config.DefaultPriorityKey
}
// First try with read lock for the common case where tracker already exists.
c.taskTrackerLock.RLock()
if tracker, ok := intervals[priorityKey]; ok {
c.taskTrackerLock.RUnlock()
return tracker
}
c.taskTrackerLock.RUnlock()
// Otherwise, we need to maybe create a new tracker with the write lock.
c.taskTrackerLock.Lock()
defer c.taskTrackerLock.Unlock()
if tracker, ok := intervals[priorityKey]; ok {
return tracker // tracker was created while we were waiting for the lock
}
// Initalize all task trackers together; or the timeframes won't line up.
c.tasksAdded[priorityKey] = newTaskTracker(c.partitionMgr.engine.timeSource)
c.tasksDispatched[priorityKey] = newTaskTracker(c.partitionMgr.engine.timeSource)
return intervals[priorityKey]
}
func aggregateStats(stats map[int32]*taskqueuepb.TaskQueueStats) *taskqueuepb.TaskQueueStats {
result := &taskqueuepb.TaskQueueStats{ApproximateBacklogAge: durationpb.New(0)}
for _, s := range stats {
mergeStats(result, s)
}
return result
}
func mergeStats(into, from *taskqueuepb.TaskQueueStats) {
into.ApproximateBacklogCount += from.ApproximateBacklogCount
into.ApproximateBacklogAge = oldestBacklogAge(into.ApproximateBacklogAge, from.ApproximateBacklogAge)
into.TasksAddRate += from.TasksAddRate
into.TasksDispatchRate += from.TasksDispatchRate
}
func oldestBacklogAge(left, right *durationpb.Duration) *durationpb.Duration {
if left.AsDuration() > right.AsDuration() {
return left
}
return right
}