Files
tasks/service/matching/physical_task_queue_manager.go
ShivamandYichao Yang 04cdc274e1 Queries on drained + poller-less version to respond with descriptive error message (#7946)
## What changed?
- Queries on drained + poller-less version to respond with descriptive
error message
- Also added `VersionStatus` as part of the userData. Detailed
explanation as to why I did that is in the file where the changes have
been made.

## Why?
- versioning

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

## Potential risks
- Version workflow now needs patching since there is an extra sync call
every-time a version's drainage status changes.
2025-08-04 18:29:54 -07:00

693 lines
25 KiB
Go

package matching
import (
"context"
"errors"
"fmt"
"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/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/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
cancelSub 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
tasksAddedInIntervals *taskTracker
tasksDispatchedInIntervals *taskTracker
deploymentRegistrationCh chan struct{}
deploymentVersionRegistered bool
pollerScalingRateLimiter quotas.RateLimiter
firstPoll time.Time
}
// TODO(pri): old matcher cleanup
matcherInterface interface {
Start()
Stop()
Rate() float64
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."
)
func newPhysicalTaskQueueManager(
partitionMgr *taskQueuePartitionManagerImpl,
queue *PhysicalTaskQueueKey,
) (*physicalTaskQueueManagerImpl, error) {
e := partitionMgr.engine
config := partitionMgr.config
buildIdTagValue := queue.Version().MetricsTagValue()
logger := log.With(partitionMgr.logger, tag.WorkerBuildId(buildIdTagValue))
throttledLogger := log.With(partitionMgr.throttledLogger, 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,
logger: logger,
throttledLogger: throttledLogger,
metricsHandler: taggedMetricsHandler,
tasksAddedInIntervals: newTaskTracker(clock.NewRealTimeSource()),
tasksDispatchedInIntervals: newTaskTracker(clock.NewRealTimeSource()),
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,
)
newMatcher, cancelSub := config.NewMatcher(func(bool) {
// unload on change to NewMatcher so that we can reload with the new setting:
pqMgr.UnloadFromPartitionManager(unloadCauseConfigChange)
})
pqMgr.cancelSub = cancelSub
if newMatcher {
pqMgr.backlogMgr = newPriBacklogManager(
tqCtx,
pqMgr,
config,
e.taskManager,
logger,
throttledLogger,
e.matchingRawClient,
newPriMetricsHandler(taggedMetricsHandler),
)
var fwdr *priForwarder
var err error
if !queue.Partition().IsRoot() && queue.Partition().Kind() != enumspb.TASK_QUEUE_KIND_STICKY {
// 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,
logger,
newPriMetricsHandler(taggedMetricsHandler),
)
pqMgr.matcher = pqMgr.priMatcher
} else {
pqMgr.backlogMgr = newBacklogManager(
tqCtx,
pqMgr,
config,
e.taskManager,
logger,
throttledLogger,
e.matchingRawClient,
taggedMetricsHandler,
)
var fwdr *Forwarder
var err error
if !queue.Partition().IsRoot() && queue.Partition().Kind() != enumspb.TASK_QUEUE_KIND_STICKY {
// 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.rateLimiter)
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
}
c.cancelSub()
// 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 the priority matcher is enabled, use the rate limiter defined in the priority matcher.
// TODO(pri): remove this once we have a way to set the partition-scoped rate limiter for the priority matcher.
if rps := pollMetadata.taskQueueMetadata.GetMaxTasksPerSecond(); rps != nil {
if c.priMatcher != nil {
c.priMatcher.UpdateRatelimit(rps.Value)
}
}
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) {
c.metricsHandler.Counter(metrics.ExpiredTasksPerTaskQueueCounter.Name()).Record(1)
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.tasksDispatchedInIntervals.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)
c.metricsHandler.Counter(metrics.ExpiredTasksPerTaskQueueCounter.Name()).Record(1)
// Don't try to set read level here because it may have been advanced already.
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)
if !task.isForwarded() {
c.tasksAddedInIntervals.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)
if !task.isForwarded() {
c.tasksAddedInIntervals.incrementTaskCount()
}
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()
response.DescResponse.TaskQueueStatus.RatePerSecond = c.matcher.Rate()
}
return response
}
func (c *physicalTaskQueueManagerImpl) GetStats() *taskqueuepb.TaskQueueStats {
return &taskqueuepb.TaskQueueStats{
ApproximateBacklogCount: c.backlogMgr.TotalApproximateBacklogCount(),
// using this and not matcher's because it reports only the age of the current physical
// queue backlog (not including the redirected backlogs) which is consistent with the
// ApproximateBacklogCount metric.
ApproximateBacklogAge: durationpb.New(c.backlogMgr.BacklogHeadAge()),
TasksAddRate: c.tasksAddedInIntervals.rate(),
TasksDispatchRate: c.tasksDispatchedInIntervals.rate(),
}
}
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.tasksAddedInIntervals.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 := newChildContext(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
}
// newChildContext creates a child context with desired timeout.
// if tailroom is non-zero, then child context timeout will be
// the minOf(parentCtx.Deadline()-tailroom, timeout). Use this
// method to create child context when childContext cannot use
// all of parent's deadline but instead there is a need to leave
// some time for parent to do some post-work
func newChildContext(
parent context.Context,
timeout time.Duration,
tailroom time.Duration,
) (context.Context, context.CancelFunc) {
if parent.Err() != nil {
return parent, func() {}
}
deadline, ok := parent.Deadline()
if !ok {
return context.WithTimeout(parent, timeout)
}
remaining := time.Until(deadline) - tailroom
if remaining < timeout {
timeout = max(0, remaining)
}
return context.WithTimeout(parent, timeout)
}
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, c.GetStats)
}
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,
}
}