Files
1e31c76ed1 Versioning cherry-pick for OSS v1.30.2 (#9543)
## What changed?
Cherry-pick versioning PRs
- #9168
  - Cache for system protection
- #9262 
  - Cache for system protection
- #9239 
- Critical PR to enable sending `TargetVersionChanged` flag for
Upgrade-on-CaN feature
- #9147 
- Tracks version drainage properly when version receives workflows via
`VersioningOverride`. Needed for automated worker controllers to
correctly scale versioned workers that received workflows via
`VersioningOverride`.
- #9300 
- Needed for `approximate_backlog_count` metric to track Current and
Ramping version tasks correctly
- #9316 
- Needed for `approximate_backlog_count` metric to track Current and
Ramping version tasks correctly
- #8957 
- Contains minor metric improvement. Included because it adds a test
harness that is used in the two metrics PRs above
- #9250 
  - Bug fix of task rescheduling edge case during AutoUpgrade Transition

## Why?
For OSS v1.30.2

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





---

> [!NOTE]
> **Medium Risk**
> Touches history/matching worker-versioning paths, adding new caches
and changing workflow task/start handling and backlog metric emission;
incorrect caching or signaling could affect dispatch/upgrade behavior
and observability.
> 
> **Overview**
> Adds new worker-versioning protections and upgrade signaling: workflow
task started events now persist a
`workflow_task_target_worker_deployment_version_changed` flag (and emit
a new `workflow_target_version_changed_count` metric) under a new
`EnableSendTargetVersionChanged` dynamic config.
> 
> Introduces two new caches with metrics and dynamic config knobs: a
`RoutingInfoCache` to avoid repeated `GetTaskQueueUserData` lookups
during activity start/transition logic, and a `ReactivationSignalCache`
plus `EnableVersionReactivationSignals` to dedupe and asynchronously
send “reactivation” signals when workflows are pinned (via
start/signal-with-start/reset/update-options) to potentially
drained/inactive worker versions.
> 
> Extends matching backlog metrics to support version-attributed
reporting by adding `BacklogMetricsEmitInterval` and switching queue DB
emission to *physical* backlog gauges (`physical_approximate_backlog_*`)
when attribution is enabled, while keeping legacy gauges when disabled.
> 
> Adds frontend scaffolding for a new visibility RPC `CountSchedules`
(client plumbing, interception/metadata/quota wiring) but leaves the
frontend handler unimplemented, and bumps `go.temporal.io/api` to
`v1.62.2`.
> 
> <sup>Written by [Cursor
Bugbot]() for commit
cb8ae14c86666654c51dd66ae91cb225722e1ba7. This will update automatically
on new commits. Configure
[here]().</sup>

---------

Co-authored-by: Shahab Tajik <[email protected]>
Co-authored-by: Shivam <[email protected]>
2026-03-17 17:19:47 -05:00

286 lines
9.9 KiB
Go

package updateworkflowoptions
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
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"
workflowpb "go.temporal.io/api/workflow/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/server/api/historyservice/v1"
"go.temporal.io/server/api/matchingservice/v1"
"go.temporal.io/server/api/matchingservicemock/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/chasm"
"go.temporal.io/server/common/cluster"
"go.temporal.io/server/common/cluster/clustertest"
"go.temporal.io/server/common/locks"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/service/history/api"
historyi "go.temporal.io/server/service/history/interfaces"
"go.temporal.io/server/service/history/tests"
wcache "go.temporal.io/server/service/history/workflow/cache"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
type noopVersionMembershipCache struct{}
func (noopVersionMembershipCache) Get(
_ string,
_ string,
_ enumspb.TaskQueueType,
_ string,
_ string,
) (isMember bool, ok bool) {
return false, false
}
func (noopVersionMembershipCache) Put(
_ string,
_ string,
_ enumspb.TaskQueueType,
_ string,
_ string,
_ bool,
) {
}
type noopReactivationSignalCache struct{}
func (noopReactivationSignalCache) ShouldSendSignal(_, _, _ string) bool {
return false // Always return false to skip sending signals in tests
}
// noopReactivationSignaler is a no-op signaler function for tests
func noopReactivationSignaler(_ context.Context, _ *namespace.Namespace, _, _ string) error {
return nil
}
var (
emptyOptions = &workflowpb.WorkflowExecutionOptions{}
unpinnedOverrideOptions = &workflowpb.WorkflowExecutionOptions{
VersioningOverride: &workflowpb.VersioningOverride{
Behavior: enumspb.VERSIONING_BEHAVIOR_AUTO_UPGRADE,
},
}
pinnedOverrideOptionsA = &workflowpb.WorkflowExecutionOptions{
VersioningOverride: &workflowpb.VersioningOverride{
Behavior: enumspb.VERSIONING_BEHAVIOR_PINNED,
PinnedVersion: "X.A",
},
}
pinnedOverrideOptionsB = &workflowpb.WorkflowExecutionOptions{
VersioningOverride: &workflowpb.VersioningOverride{
Behavior: enumspb.VERSIONING_BEHAVIOR_PINNED,
PinnedVersion: "X.B",
},
}
)
func TestMergeOptions_VersionOverrideMask(t *testing.T) {
updateMask := &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}}
input := emptyOptions
// Merge unpinned into empty options
merged, err := mergeWorkflowExecutionOptions(input, unpinnedOverrideOptions, updateMask)
if err != nil {
t.Error(err)
}
assert.EqualExportedValues(t, unpinnedOverrideOptions, merged)
// Merge pinned_A into unpinned options
merged, err = mergeWorkflowExecutionOptions(input, pinnedOverrideOptionsA, updateMask)
if err != nil {
t.Error(err)
}
assert.EqualExportedValues(t, pinnedOverrideOptionsA, merged)
// Merge pinned_B into pinned_A options
merged, err = mergeWorkflowExecutionOptions(input, pinnedOverrideOptionsB, updateMask)
if err != nil {
t.Error(err)
}
assert.EqualExportedValues(t, pinnedOverrideOptionsB, merged)
// Unset versioning override
merged, err = mergeWorkflowExecutionOptions(input, emptyOptions, updateMask)
if err != nil {
t.Error(err)
}
assert.EqualExportedValues(t, emptyOptions, merged)
}
func TestMergeOptions_PartialMask(t *testing.T) {
bothUpdateMask := &fieldmaskpb.FieldMask{Paths: []string{"versioning_override.behavior", "versioning_override.deployment"}}
behaviorOnlyUpdateMask := &fieldmaskpb.FieldMask{Paths: []string{"versioning_override.behavior"}}
deploymentOnlyUpdateMask := &fieldmaskpb.FieldMask{Paths: []string{"versioning_override.deployment"}}
_, err := mergeWorkflowExecutionOptions(emptyOptions, unpinnedOverrideOptions, behaviorOnlyUpdateMask)
assert.Error(t, err)
_, err = mergeWorkflowExecutionOptions(emptyOptions, unpinnedOverrideOptions, deploymentOnlyUpdateMask)
assert.Error(t, err)
merged, err := mergeWorkflowExecutionOptions(emptyOptions, unpinnedOverrideOptions, bothUpdateMask)
assert.NoError(t, err)
assert.EqualExportedValues(t, unpinnedOverrideOptions, merged)
}
func TestMergeOptions_EmptyMask(t *testing.T) {
emptyUpdateMask := &fieldmaskpb.FieldMask{Paths: []string{}}
input := pinnedOverrideOptionsB
// Don't merge anything
merged, err := mergeWorkflowExecutionOptions(input, pinnedOverrideOptionsA, emptyUpdateMask)
assert.NoError(t, err)
assert.EqualExportedValues(t, input, merged)
// Don't merge anything
merged, err = mergeWorkflowExecutionOptions(input, nil, emptyUpdateMask)
assert.NoError(t, err)
assert.EqualExportedValues(t, input, merged)
}
func TestMergeOptions_AsteriskMask(t *testing.T) {
asteriskUpdateMask := &fieldmaskpb.FieldMask{Paths: []string{"*"}}
_, err := mergeWorkflowExecutionOptions(emptyOptions, unpinnedOverrideOptions, asteriskUpdateMask)
assert.Error(t, err)
}
func TestMergeOptions_FooMask(t *testing.T) {
fooUpdateMask := &fieldmaskpb.FieldMask{Paths: []string{"foo"}}
_, err := mergeWorkflowExecutionOptions(emptyOptions, unpinnedOverrideOptions, fooUpdateMask)
assert.Error(t, err)
}
type (
// updateWorkflowOptionsSuite contains tests for the UpdateWorkflowOptions API.
updateWorkflowOptionsSuite struct {
suite.Suite
*require.Assertions
controller *gomock.Controller
shardContext *historyi.MockShardContext
namespaceRegistry *namespace.MockRegistry
workflowCache *wcache.MockCache
workflowConsistencyChecker api.WorkflowConsistencyChecker
currentContext *historyi.MockWorkflowContext
currentMutableState *historyi.MockMutableState
mockMatchingClient *matchingservicemock.MockMatchingServiceClient
}
)
func TestUpdateWorkflowOptionsSuite(t *testing.T) {
s := new(updateWorkflowOptionsSuite)
suite.Run(t, s)
}
func (s *updateWorkflowOptionsSuite) SetupTest() {
s.Assertions = require.New(s.T())
s.controller = gomock.NewController(s.T())
s.namespaceRegistry = namespace.NewMockRegistry(s.controller)
s.namespaceRegistry.EXPECT().GetNamespaceByID(tests.GlobalNamespaceEntry.ID()).Return(tests.GlobalNamespaceEntry, nil)
s.shardContext = historyi.NewMockShardContext(s.controller)
s.shardContext.EXPECT().GetNamespaceRegistry().Return(s.namespaceRegistry)
s.shardContext.EXPECT().GetClusterMetadata().Return(clustertest.NewMetadataForTest(cluster.NewTestClusterMetadataConfig(true, true)))
s.shardContext.EXPECT().GetConfig().Return(tests.NewDynamicConfig()).AnyTimes()
// mock a mutable state with an existing versioning override
s.currentMutableState = historyi.NewMockMutableState(s.controller)
s.currentMutableState.EXPECT().GetExecutionInfo().Return(&persistencespb.WorkflowExecutionInfo{
WorkflowId: tests.WorkflowID,
VersioningInfo: &workflowpb.WorkflowExecutionVersioningInfo{
VersioningOverride: &workflowpb.VersioningOverride{
Behavior: enumspb.VERSIONING_BEHAVIOR_AUTO_UPGRADE,
PinnedVersion: "X.123",
},
},
}).AnyTimes()
s.currentMutableState.EXPECT().GetExecutionState().Return(&persistencespb.WorkflowExecutionState{
RunId: tests.RunID,
}).AnyTimes()
s.currentContext = historyi.NewMockWorkflowContext(s.controller)
s.currentContext.EXPECT().LoadMutableState(gomock.Any(), s.shardContext).Return(s.currentMutableState, nil)
s.workflowCache = wcache.NewMockCache(s.controller)
s.workflowCache.EXPECT().GetOrCreateChasmExecution(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), chasm.WorkflowArchetypeID, locks.PriorityHigh).
Return(s.currentContext, wcache.NoopReleaseFn, nil)
s.workflowConsistencyChecker = api.NewWorkflowConsistencyChecker(
s.shardContext,
s.workflowCache,
)
s.mockMatchingClient = matchingservicemock.NewMockMatchingServiceClient(s.controller)
}
func (s *updateWorkflowOptionsSuite) TearDownTest() {
s.controller.Finish()
}
func (s *updateWorkflowOptionsSuite) TestInvoke_Success() {
expectedOverrideOptions := &workflowpb.WorkflowExecutionOptions{
VersioningOverride: &workflowpb.VersioningOverride{
Override: &workflowpb.VersioningOverride_Pinned{
Pinned: &workflowpb.VersioningOverride_PinnedOverride{
Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED,
Version: &deploymentpb.WorkerDeploymentVersion{
DeploymentName: "X",
BuildId: "A",
},
},
},
},
}
s.currentMutableState.EXPECT().IsWorkflowExecutionRunning().Return(true)
s.mockMatchingClient.EXPECT().CheckTaskQueueVersionMembership(
gomock.Any(),
gomock.Any(),
).Return(&matchingservice.CheckTaskQueueVersionMembershipResponse{
IsMember: true,
}, nil)
s.currentMutableState.EXPECT().AddWorkflowExecutionOptionsUpdatedEvent(expectedOverrideOptions.VersioningOverride, false, "", nil, nil, "", expectedOverrideOptions.Priority).Return(&historypb.HistoryEvent{}, nil)
s.currentContext.EXPECT().UpdateWorkflowExecutionAsActive(gomock.Any(), s.shardContext).Return(nil)
updateReq := &historyservice.UpdateWorkflowExecutionOptionsRequest{
NamespaceId: tests.NamespaceID.String(),
UpdateRequest: &workflowservice.UpdateWorkflowExecutionOptionsRequest{
Namespace: tests.Namespace.String(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: tests.WorkflowID,
RunId: tests.RunID,
},
WorkflowExecutionOptions: expectedOverrideOptions,
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"versioning_override"}},
},
}
resp, err := Invoke(
context.Background(),
updateReq,
s.shardContext,
s.workflowConsistencyChecker,
s.mockMatchingClient,
noopVersionMembershipCache{}, // cache not meant to be used in this test
noopReactivationSignalCache{}, // cache not meant to be used in this test
noopReactivationSignaler, // signaler not meant to be used in this test
)
s.NoError(err)
s.NotNil(resp)
proto.Equal(expectedOverrideOptions, resp.GetWorkflowExecutionOptions())
}