mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## 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]>
286 lines
9.9 KiB
Go
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())
|
|
}
|