Files
tasks/tests/workflow_alias_search_attribute_test.go
ShivamandGitHub cf61171613 Reject versioning override when version does not exist (#8791)
## What changed?
- WISOTT
- Also added a cache per history host so that we don't overburden
matching with these calls.
- Also added a whole new unit test testing the function
`ValidateVersioningOverride`
- TODO in a follow-up PR: add metrics for this cache. Doing this as a
follow-up in the interest of time but have it tracked in JIRA.

## Why?
- Versioning correctness.

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

## Potential risks
- I don't think this is risky


---

> [!NOTE]
> Validate versioning overrides by ensuring pinned versions exist in the
task queue via matching RPC with a per-host cache, refactor validation
into history APIs, and add configs and tests.
> 
> - **Worker Versioning / Validation**:
> - Add `ValidateVersioningOverride(ctx, ...)` to verify pinned versions
exist in a task queue via `matching.CheckTaskQueueVersionMembership`.
> - Introduce per-host `versionMembershipCache` to cache membership
results; reject with `FailedPrecondition` if not present.
> - Remove frontend/batcher inline override validation; perform it in
history layer (start, signal-with-start, update options, reset
post-ops).
> - **History Service Wiring**:
> - Thread `matchingClient` and `versionMembershipCache` through history
engine, starter, multi-op, signal-with-start, reset, and
update-workflow-options APIs.
> - Mark batch UpdateWorkflowOptions non-retryable for "Pinned version
is not present in the task queue".
> - **Config / Dynamic Config**:
> - Add `history.versionMembershipCacheTTL` and
`history.versionMembershipCacheMaxSize`; provide cache in `fx` with
lifecycle management.
> - **Testing**:
> - Add unit tests for `ValidateVersioningOverride` covering cache
hits/misses and v0.31/v0.32 paths.
> - Extend functional tests to assert membership checks, cache behavior,
batch update failures, and reset with post-reset options.
> - Test helpers: `TestVars.WithDeploymentSeries`, `WithBuildID`, and
utilities to ensure versions are present via matching RPC.
> 
> <sup>Written by [Cursor
Bugbot]() for commit
d8b755273fe41c823a30c4cc2ecbbeb2c2be124d. This will update automatically
on new commits. Configure
[here]().</sup>
2025-12-19 11:18:19 -05:00

229 lines
7.7 KiB
Go

package tests
import (
"context"
"fmt"
"testing"
"time"
"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"
"go.temporal.io/api/operatorservice/v1"
taskqueuepb "go.temporal.io/api/taskqueue/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/workflow"
deploymentspb "go.temporal.io/server/api/deployment/v1"
"go.temporal.io/server/api/matchingservice/v1"
"go.temporal.io/server/common/payload"
"go.temporal.io/server/common/searchattribute/sadefs"
"go.temporal.io/server/common/testing/testvars"
"go.temporal.io/server/tests/testcore"
)
type WorkflowAliasSearchAttributeTestSuite struct {
testcore.FunctionalTestBase
}
func TestWorkflowAliasSearchAttributeTestSuite(t *testing.T) {
s := new(WorkflowAliasSearchAttributeTestSuite)
suite.Run(t, s)
}
func (s *WorkflowAliasSearchAttributeTestSuite) SetupTest() {
s.FunctionalTestBase.SetupTest()
s.Worker().RegisterWorkflow(s.workflowFunc)
}
func (s *WorkflowAliasSearchAttributeTestSuite) workflowFunc(ctx workflow.Context) (string, error) {
return "done!", nil
}
func (s *WorkflowAliasSearchAttributeTestSuite) startVersionedPollerAndValidate(
ctx context.Context,
taskQueueName string,
deploymentName string,
buildID string,
) {
taskQueue := &taskqueuepb.TaskQueue{
Name: taskQueueName,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
// Start versioned poller in background
go func() {
_, _ = s.FrontendClient().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: taskQueue,
Identity: "versioned-poller",
DeploymentOptions: &deploymentpb.WorkerDeploymentOptions{
DeploymentName: deploymentName,
BuildId: buildID,
WorkerVersioningMode: enumspb.WORKER_VERSIONING_MODE_VERSIONED,
},
})
}()
// Validate version is present via matching RPC
version := &deploymentspb.WorkerDeploymentVersion{
DeploymentName: deploymentName,
BuildId: buildID,
}
s.EventuallyWithT(func(t *assert.CollectT) {
a := require.New(t)
resp, err := s.GetTestCluster().MatchingClient().CheckTaskQueueVersionMembership(
ctx,
&matchingservice.CheckTaskQueueVersionMembershipRequest{
NamespaceId: s.NamespaceID().String(),
TaskQueue: taskQueueName,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
Version: version,
},
)
a.NoError(err)
a.True(resp.GetIsMember())
}, 10*time.Second, 1*time.Second)
}
func (s *WorkflowAliasSearchAttributeTestSuite) createWorkflow(
ctx context.Context,
tv *testvars.TestVars,
sa *commonpb.SearchAttributes,
) (*workflowservice.StartWorkflowExecutionResponse, error) {
// Start a versioned poller so that the version, which will be set as an override, is present in the task queue.
s.startVersionedPollerAndValidate(ctx, tv.TaskQueue().Name, tv.DeploymentSeries(), tv.BuildID())
request := &workflowservice.StartWorkflowExecutionRequest{
RequestId: tv.Any().String(),
Namespace: s.Namespace().String(),
WorkflowId: tv.WorkflowID(),
WorkflowType: tv.WorkflowType(),
TaskQueue: tv.TaskQueue(),
Identity: tv.WorkerIdentity(),
VersioningOverride: tv.VersioningOverridePinned(true),
SearchAttributes: sa,
}
return s.FrontendClient().StartWorkflowExecution(ctx, request)
}
func (s *WorkflowAliasSearchAttributeTestSuite) terminateWorkflow(
ctx context.Context,
tv *testvars.TestVars,
) (*workflowservice.TerminateWorkflowExecutionResponse, error) {
return s.FrontendClient().TerminateWorkflowExecution(ctx, &workflowservice.TerminateWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: tv.WorkflowID(),
},
Reason: "terminate reason",
})
}
func (s *WorkflowAliasSearchAttributeTestSuite) TestWorkflowAliasSearchAttribute() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Overriding the task queue name, deployment name, and build ID so that we don't hit length limitations
// for any of these in any of the databases.
tv := testvars.New(s.T()).
WithTaskQueue("tq-alias-sa").
WithDeploymentSeries("dep-alias-sa").
WithBuildID("build-alias-sa")
_, err := s.createWorkflow(ctx, tv, nil)
s.Require().NoError(err)
s.EventuallyWithT(
func(t *assert.CollectT) {
// Filter by WorkflowId to isolate this test's workflow from other tests
resp, err := s.SdkClient().ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
Namespace: s.Namespace().String(),
Query: fmt.Sprintf("WorkflowId = '%s'", tv.WorkflowID()),
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Len(t, resp.GetExecutions(), 1)
queriedResp, err := s.SdkClient().ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
Namespace: s.Namespace().String(),
Query: fmt.Sprintf("%s = 'Pinned' AND WorkflowId = '%s'", sadefs.TemporalWorkflowVersioningBehavior, tv.WorkflowID()),
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Len(t, queriedResp.GetExecutions(), 1)
queriedResp, err = s.SdkClient().ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
Namespace: s.Namespace().String(),
Query: fmt.Sprintf("WorkflowVersioningBehavior = 'Pinned' AND WorkflowId = '%s'", tv.WorkflowID()),
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Len(t, queriedResp.GetExecutions(), 1)
},
testcore.WaitForESToSettle,
100*time.Millisecond,
)
_, err = s.terminateWorkflow(ctx, tv)
s.Require().NoError(err)
}
func (s *WorkflowAliasSearchAttributeTestSuite) TestWorkflowAliasSearchAttribute_CustomSearchAttributeOverride() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Overriding the task queue name, deployment name, and build ID so that we don't hit length limitations
// for any of these in any of the databases.
tv := testvars.New(s.T()).
WithTaskQueue("tq-alias-sa-custom").
WithDeploymentSeries("dep-alias-sa-custom").
WithBuildID("build-alias-sa-custom")
_, err := s.SdkClient().OperatorService().AddSearchAttributes(ctx, &operatorservice.AddSearchAttributesRequest{
Namespace: s.Namespace().String(),
SearchAttributes: map[string]enumspb.IndexedValueType{
"WorkflowVersioningBehavior": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
},
})
s.Require().NoError(err)
sa := &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{
"WorkflowVersioningBehavior": payload.EncodeString("user-defined"),
},
}
_, err = s.createWorkflow(ctx, tv, sa)
s.Require().NoError(err)
s.EventuallyWithT(
func(t *assert.CollectT) {
// Filter by WorkflowId to isolate this test's workflow from other tests
queriedResp, err := s.SdkClient().ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
Namespace: s.Namespace().String(),
Query: fmt.Sprintf("%s = 'Pinned' AND WorkflowId = '%s'", sadefs.TemporalWorkflowVersioningBehavior, tv.WorkflowID()),
})
require.NoError(t, err)
require.NotNil(t, queriedResp)
require.Len(t, queriedResp.GetExecutions(), 1)
queriedResp, err = s.SdkClient().ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
Namespace: s.Namespace().String(),
Query: fmt.Sprintf("WorkflowVersioningBehavior = 'user-defined' AND WorkflowId = '%s'", tv.WorkflowID()),
})
require.NoError(t, err)
require.NotNil(t, queriedResp)
require.Len(t, queriedResp.GetExecutions(), 1)
},
testcore.WaitForESToSettle,
100*time.Millisecond,
)
_, err = s.terminateWorkflow(ctx, tv)
s.Require().NoError(err)
}