Fix sticky queue stats zeroed out (#11527)

## What

Skip the special `versioning attribution logic` when computing stats for
sticky queues. Otherwise, we end up with 0 value for add and dispatch
rates. This breaks poller autoscaling for sticky queues since 0/0 = NaN,
and NaN > threshold is always false, so no +1 scaling signals are ever
emitted.

Background on version attribution logic
----------------------------------------
For a versioned deployment, when tasks arrive, they are always added to
the unversioned queue. Then at dispatch time, they are routed to the
versioned queue. So the unversioned queue stats includes tasks that
belong to versioned queue. When reporting stats for a queue, we need to
remove the versioned queue stats from unversioned queue stats to avoid
double counting.

Why this is not applicable for sticky queue (any unversioned queue)
--------------------------------------------------------------------
Sticky queues are not versioned; they only have the unversioned physical
queue. So the above logic is not applicable. It was querying the stats
for same queue twice (build_id = "") and subtracting them -- resulting
in a value of 0.


## How did you test it?

Unit tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kannan
2026-08-14 12:30:08 -07:00
committed by GitHub
parent df0e81eaee
commit 55cf6be564
2 changed files with 116 additions and 13 deletions

View File

@@ -1459,20 +1459,24 @@ func (pm *taskQueuePartitionManagerImpl) describe(
isRampingDescribe := deploymentVersion.GetDeploymentName() == rampingVersion.GetDeploymentName() &&
deploymentVersion.GetBuildId() == rampingVersion.GetBuildId()
if isUnversionedDescribe {
// Reduce unversioned stats by any shares attributed to versioned queues.
if currentExists {
subtractStatsByPriority(adjustedStatsByPriority, unversionedCurrentShareByPriority)
// Skip versioning attribution for partitions that don't support versioning
// (e.g. sticky queues): their stats belong entirely to the queue itself.
if pm.partition.SupportsVersioning() {
if isUnversionedDescribe {
// Reduce unversioned stats by any shares attributed to versioned queues.
if currentExists {
subtractStatsByPriority(adjustedStatsByPriority, unversionedCurrentShareByPriority)
}
// Only subtract the ramping share when ramping is to a versioned deployment. If ramping is to
// unversioned, that share should remain part of the unversioned queue stats.
if rampingExists && !isRampingToUnversioned {
subtractStatsByPriority(adjustedStatsByPriority, unversionedRampingShareByPriority)
}
} else if isCurrentDescribe && currentExists {
mergeStatsByPriority(adjustedStatsByPriority, unversionedCurrentShareByPriority)
} else if isRampingDescribe && rampingExists {
mergeStatsByPriority(adjustedStatsByPriority, unversionedRampingShareByPriority)
}
// Only subtract the ramping share when ramping is to a versioned deployment. If ramping is to
// unversioned, that share should remain part of the unversioned queue stats.
if rampingExists && !isRampingToUnversioned {
subtractStatsByPriority(adjustedStatsByPriority, unversionedRampingShareByPriority)
}
} else if isCurrentDescribe && currentExists {
mergeStatsByPriority(adjustedStatsByPriority, unversionedCurrentShareByPriority)
} else if isRampingDescribe && rampingExists {
mergeStatsByPriority(adjustedStatsByPriority, unversionedRampingShareByPriority)
}
vInfo.PhysicalTaskQueueInfo.TaskQueueStatsByPriorityKey = adjustedStatsByPriority

View File

@@ -23,6 +23,7 @@ import (
"go.temporal.io/server/api/matchingservicemock/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
taskqueuespb "go.temporal.io/server/api/taskqueue/v1"
"go.temporal.io/server/common/clock"
hlc "go.temporal.io/server/common/clock/hybrid_logical_clock"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/metrics"
@@ -2275,3 +2276,101 @@ func TestSplitTaskQueueStatsByRampPercentage_RateLimitingFalse(t *testing.T) {
require.False(t, currentShare.RateLimitingActive)
require.False(t, rampShare.RateLimitingActive)
}
// TestStickyQueueAdjustedStats_VersioningAttributionSkipped verifies that the versioning
// attribution logic in Describe is skipped for sticky queues. Sticky queues don't route
// by version, so their stats should not be reduced by the current/ramping version shares.
func TestStickyQueueAdjustedStats_VersioningAttributionSkipped(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
logger := testlogger.NewTestLogger(t, testlogger.FailOnAnyUnexpectedError)
ns, registry := createMockNamespaceCache(ctrl, namespace.Name(namespaceName))
config := defaultTestConfig()
matchingClient := matchingservicemock.NewMockMatchingServiceClient(ctrl)
matchingClient.EXPECT().ForceLoadTaskQueuePartition(gomock.Any(), gomock.Any(), gomock.Any()).
Return(&matchingservice.ForceLoadTaskQueuePartitionResponse{}, nil).AnyTimes()
engine := createTestMatchingEngine(logger, ctrl, config, matchingClient, registry)
// Use a fixed time source so rate calculations are deterministic across reads.
ts := clock.NewEventTimeSource()
ts.Update(time.Now())
engine.timeSource = ts
// Create a sticky partition (like a real worker's sticky queue)
f, err := tqid.NewTaskQueueFamily(namespaceID, taskQueueName)
require.NoError(t, err)
stickyPartition := f.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).StickyPartition("my-sticky-queue")
tqConfig := newTaskQueueConfig(stickyPartition.TaskQueue(), engine.config, ns.Name())
// Seed user data with a current deployment version (no ramping).
// This simulates a sticky queue that fetches user data from its normal queue,
// which has a current deployment configured.
const (
deploymentName = "my-deployment"
currentBuildID = "build-abc"
)
userDataMgr := &mockUserDataManager{
data: &persistencespb.VersionedTaskQueueUserData{
Data: &persistencespb.TaskQueueUserData{
PerType: map[int32]*persistencespb.TaskQueueTypeUserData{
int32(enumspb.TASK_QUEUE_TYPE_WORKFLOW): {
DeploymentData: &persistencespb.DeploymentData{
DeploymentsData: map[string]*persistencespb.WorkerDeploymentData{
deploymentName: {
RoutingConfig: &deploymentpb.RoutingConfig{
CurrentDeploymentVersion: &deploymentpb.WorkerDeploymentVersion{
DeploymentName: deploymentName,
BuildId: currentBuildID,
},
CurrentVersionChangedTime: timestamppb.New(time.Now().Add(-1 * time.Hour)),
},
Versions: map[string]*deploymentspb.WorkerDeploymentVersionData{
currentBuildID: {RevisionNumber: 1, UpdateTime: timestamppb.Now()},
},
},
},
},
},
},
},
},
}
pm, err := newTaskQueuePartitionManager(engine, ns, stickyPartition, tqConfig, logger, logger, metrics.NoopMetricsHandler, userDataMgr)
require.NoError(t, err)
engine.partitions[stickyPartition.Key()] = pm
engine.Start()
pm.Start()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
err = pm.WaitUntilInitialized(ctx)
require.NoError(t, err)
dbq := pm.defaultQueue()
require.NotNil(t, dbq)
// Simulate a task being added and dispatched on the sticky queue.
dbqImpl := dbq.(*physicalTaskQueueManagerImpl)
dbqImpl.incTaskTracker(dbqImpl.tasksAdded, 3, 1)
dbqImpl.incTaskTracker(dbqImpl.tasksDispatched, 3, 1)
// Advance time so the task tracker has positive elapsed time for rate calculation.
ts.Advance(time.Second)
// Verify the raw stats have non-zero rates (precondition for the test to be meaningful).
rawStats := dbq.GetStatsByPriority(true)
require.Greater(t, rawStats[3].TasksAddRate, float32(0))
require.Greater(t, rawStats[3].TasksDispatchRate, float32(0))
// GetPhysicalQueueAdjustedStats goes through Describe which applies versioning
// attribution. For sticky queues, attribution should be skipped, so adjusted rates
// should exactly match the raw rates.
adjustedStats := pm.GetPhysicalQueueAdjustedStats(context.Background(), dbq)
require.NotNil(t, adjustedStats)
require.InDelta(t, rawStats[3].TasksAddRate, adjustedStats.TasksAddRate, 0)
require.InDelta(t, rawStats[3].TasksDispatchRate, adjustedStats.TasksDispatchRate, 0)
}