Files
tasks/tests/versioning_test.go
Alex ShtinandGitHub e8c875c808 Refactor: remove cluster.yaml config (#7102)
## What changed?
<!-- Describe what has changed in this PR -->
Refactor: remove `cluster.yaml` config.

## Why?
<!-- Tell your future self why have you made these changes -->
It is the same as `es_cluster.yaml` but doesn't have `esConfig` section.
The intent was to use this config file when Elasticsearch is not used.
But this section is not used if SQL persistence is used, so there is no
harm to always use `es_cluster.yaml`.

I am going to significantly decrease number of config files for
functional tests (if not remove them all). This is small step in this
direction.

## How did you test it?
<!-- How have you verified this change? Tested locally? Added a unit
test? Checked in staging env? -->
Run tests.

## Potential risks
<!-- Assuming the worst case, what can be broken when deploying this
change to production? -->
No risks.

## Documentation
<!-- Have you made sure this change doesn't falsify anything currently
stated in `docs/`? If significant
new behavior is added, have you described that in `docs/`? -->
No.

## Is hotfix candidate?
<!-- Is this PR a hotfix candidate or does it require a notification to
be sent to the broader community? (Yes/No) -->
No.
2025-01-17 22:21:21 +00:00

5157 lines
176 KiB
Go

// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// nolint:revive
package tests
import (
"context"
"errors"
"fmt"
"math/rand"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/dgryski/go-farm"
"github.com/stretchr/testify/suite"
commandpb "go.temporal.io/api/command/v1"
commonpb "go.temporal.io/api/common/v1"
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/sdk/activity"
sdkclient "go.temporal.io/sdk/client"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
"go.temporal.io/server/api/adminservice/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/dynamicconfig"
"go.temporal.io/server/common/searchattribute"
"go.temporal.io/server/common/tqid"
"go.temporal.io/server/common/worker_versioning"
"go.temporal.io/server/tests/testcore"
"google.golang.org/protobuf/types/known/durationpb"
)
type VersioningIntegSuite struct {
testcore.FunctionalTestSuite
sdkClient sdkclient.Client
}
const (
partitionTreeDegree = 3
longPollTime = 5 * time.Second
// use > 2 pollers by default to expose more timing situations
numPollers = 4
// These TTLs need to be greater than the time it takes for a workflow execution status change to show up in visibility
testReachabilityCacheOpenWFsTTL = 3 * time.Millisecond
testReachabilityCacheClosedWFsTTL = 6 * time.Millisecond
)
func TestVersioningFunctionalSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(VersioningIntegSuite))
}
func (s *VersioningIntegSuite) SetupSuite() {
dynamicConfigOverrides := map[dynamicconfig.Key]any{
dynamicconfig.EnableDeployments.Key(): true,
dynamicconfig.FrontendEnableWorkerVersioningDataAPIs.Key(): true,
dynamicconfig.FrontendEnableWorkerVersioningWorkflowAPIs.Key(): true,
dynamicconfig.FrontendEnableWorkerVersioningRuleAPIs.Key(): true,
dynamicconfig.MatchingForwarderMaxChildrenPerNode.Key(): partitionTreeDegree,
dynamicconfig.TaskQueuesPerBuildIdLimit.Key(): 3,
dynamicconfig.AssignmentRuleLimitPerQueue.Key(): 10,
dynamicconfig.RedirectRuleLimitPerQueue.Key(): 10,
dynamicconfig.RedirectRuleMaxUpstreamBuildIDsPerQueue.Key(): 10,
dynamicconfig.MatchingDeletedRuleRetentionTime.Key(): 24 * time.Hour,
dynamicconfig.ReachabilityBuildIdVisibilityGracePeriod.Key(): 3 * time.Minute,
dynamicconfig.ReachabilityQueryBuildIdLimit.Key(): 4,
dynamicconfig.ReachabilityCacheOpenWFsTTL.Key(): testReachabilityCacheOpenWFsTTL,
dynamicconfig.ReachabilityCacheClosedWFsTTL.Key(): testReachabilityCacheClosedWFsTTL,
// Make sure we don't hit the rate limiter in tests
dynamicconfig.FrontendGlobalNamespaceNamespaceReplicationInducingAPIsRPS.Key(): 1000,
dynamicconfig.FrontendMaxNamespaceNamespaceReplicationInducingAPIsBurstRatioPerInstance.Key(): 1,
dynamicconfig.FrontendNamespaceReplicationInducingAPIsRPS.Key(): 1000,
// The dispatch tests below rely on being able to see the effects of changing
// versioning data relatively quickly. In general, we only promise to act on new
// versioning data "soon", i.e. after a long poll interval. We can reduce the long poll
// interval so that we don't have to wait so long.
// TODO: after cleaning up old versioning we may not need to depend on long poll time. [cleanup-old-wv]
// This is because we don't interrupt long polls on change of versioning
// data, so if polls from different build ids go to the same matcher, old ones can steal
// tasks from new ones. If polls from different build ids never go to the same matcher
// anymore then we don't need it.
dynamicconfig.MatchingLongPollExpirationInterval.Key(): longPollTime,
// this is overridden for tests using testWithMatchingBehavior
dynamicconfig.MatchingNumTaskqueueReadPartitions.Key(): 4,
dynamicconfig.MatchingNumTaskqueueWritePartitions.Key(): 4,
// this is overridden since we don't want caching to be enabled while testing DescribeTaskQueue
// behaviour related to versioning
dynamicconfig.TaskQueueInfoByBuildIdTTL.Key(): 0 * time.Second,
}
s.FunctionalTestBase.SetupSuiteWithDefaultCluster(testcore.WithDynamicConfigOverrides(dynamicConfigOverrides))
}
func (s *VersioningIntegSuite) SetupTest() {
s.FunctionalTestSuite.SetupTest()
var err error
s.sdkClient, err = sdkclient.Dial(sdkclient.Options{
HostPort: s.FrontendGRPCAddress(),
Namespace: s.Namespace().String(),
})
s.NoError(err)
}
func (s *VersioningIntegSuite) TearDownTest() {
if s.sdkClient != nil {
s.sdkClient.Close()
}
}
func (s *VersioningIntegSuite) TestVersionRuleConflictToken() {
ctx := testcore.NewContext()
tq := "test-conflict-token"
// nil token --> fail
s.insertAssignmentRule(ctx, tq, "1", 0, nil, false)
// correct token from List --> success
cT1 := s.getVersioningRules(ctx, tq).GetConflictToken()
cT2 := s.insertAssignmentRule(ctx, tq, "2", 0, cT1, true)
// confirm token changed on insert but not on list
cT3 := s.getVersioningRules(ctx, tq).GetConflictToken()
s.NotEqual(cT1, cT2)
s.Equal(cT2, cT3)
// correct token from List or most recent mutation --> success
cT4 := s.insertAssignmentRule(ctx, tq, "3", 0, cT2, true)
s.getVersioningRules(ctx, tq)
// wrong token fails, same request with nil token also fails
s.insertAssignmentRule(ctx, tq, "4", 0, cT1, false)
s.insertAssignmentRule(ctx, tq, "4", 0, nil, false)
// wrong token fails, same request with correct token from Update succeeds
s.replaceAssignmentRule(ctx, tq, "20", 0, cT2, false)
cT5 := s.replaceAssignmentRule(ctx, tq, "20", 0, cT4, true)
cT6 := s.getVersioningRules(ctx, tq).GetConflictToken()
// confirm that list didn't change the conflict token again
s.Equal(cT5, cT6)
// wrong token fails, same request with correct token from List succeeds
s.deleteAssignmentRule(ctx, tq, 0, cT4, false)
s.deleteAssignmentRule(ctx, tq, 0, cT6, true)
// nil token fails
s.deleteAssignmentRule(ctx, tq, 0, nil, false)
}
func (s *VersioningIntegSuite) TestVersionRuleBuildIdValidation() {
// setup
ctx := testcore.NewContext()
tq := "test-build-id-validation"
// get initial conflict token
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
// failure due to long build id
longStr := `_1234567890_2234567890_3234567890_4234567890_5234567890_6234567890_7234567890_8234567890_9234567890_
_1234567890_2234567890_3234567890_4234567890_5234567890_6234567890_7234567890_8234567890_9234567890_
_1234567890_2234567890_3234567890_4234567890_5234567890_6234567890_7234567890_8234567890_9234567890_`
s.insertAssignmentRule(ctx, tq, longStr, 0, cT, false)
s.insertRedirectRule(ctx, tq, longStr, "foo", cT, false)
s.insertRedirectRule(ctx, tq, "foo", longStr, cT, false)
// failure due to invalid utf-8
s.insertAssignmentRule(ctx, tq, testcore.InvalidUTF8, 0, cT, false)
s.insertRedirectRule(ctx, tq, testcore.InvalidUTF8, "foo", cT, false)
s.insertRedirectRule(ctx, tq, "foo", testcore.InvalidUTF8, cT, false)
}
func (s *VersioningIntegSuite) TestAssignmentRuleInsert() {
// setup
ctx := testcore.NewContext()
tq := "test-assignment-rule-insert"
// get initial conflict token
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
// success
cT = s.insertAssignmentRule(ctx, tq, "1", 0, cT, true)
res1 := s.getVersioningRules(ctx, tq)
s.Equal("1", res1.GetAssignmentRules()[0].GetRule().GetTargetBuildId())
// failure due to out of bounds index
s.insertAssignmentRule(ctx, tq, "2", -1, cT, false)
s.Equal(res1, s.getVersioningRules(ctx, tq))
// success with conflict token returned by last successful call, same as above
s.insertAssignmentRule(ctx, tq, "2", 1, cT, true)
s.Equal("2", s.getVersioningRules(ctx, tq).GetAssignmentRules()[1].GetRule().GetTargetBuildId())
}
func (s *VersioningIntegSuite) TestAssignmentRuleReplace() {
// setup
ctx := testcore.NewContext()
tq := "test-assignment-rule-replace"
// get initial conflict token + do initial inserts
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
cT = s.insertAssignmentRule(ctx, tq, "1", 0, cT, true)
cT = s.insertAssignmentRule(ctx, tq, "2", 0, cT, true)
// success
cT = s.replaceAssignmentRule(ctx, tq, "3", 0, cT, true)
res := s.getVersioningRules(ctx, tq)
s.Equal("3", res.GetAssignmentRules()[0].GetRule().GetTargetBuildId())
// failure due to index out of bounds
s.replaceAssignmentRule(ctx, tq, "4", 10, cT, false)
s.Equal(res, s.getVersioningRules(ctx, tq))
// success with conflict token returned by last successful call, same as above
s.replaceAssignmentRule(ctx, tq, "4", 0, cT, true)
s.Equal("4", s.getVersioningRules(ctx, tq).GetAssignmentRules()[0].GetRule().GetTargetBuildId())
}
func (s *VersioningIntegSuite) TestAssignmentRuleDelete() {
// setup
ctx := testcore.NewContext()
tq := "test-assignment-rule-delete"
// get initial conflict token + do initial inserts
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
cT = s.insertAssignmentRule(ctx, tq, "1", 0, cT, true)
cT = s.insertAssignmentRule(ctx, tq, "2", 0, cT, true)
// success
cT = s.deleteAssignmentRule(ctx, tq, 0, cT, true)
res := s.getVersioningRules(ctx, tq)
s.Equal(1, len(res.GetAssignmentRules()))
// failure due to requirement that once a fully-ramped rule exists, at least one must always exist
s.deleteAssignmentRule(ctx, tq, 0, cT, false)
s.Equal(res, s.getVersioningRules(ctx, tq))
// insert another rule to prove that the conflict token was not the issue above
cT = s.insertAssignmentRule(ctx, tq, "2", 0, cT, true)
// delete again, success
s.deleteAssignmentRule(ctx, tq, 0, cT, true)
s.Equal(1, len(res.GetAssignmentRules()))
}
func (s *VersioningIntegSuite) TestRedirectRuleInsert() {
// setup
ctx := testcore.NewContext()
tq := "test-redirect-rule-insert"
// get initial conflict token
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
// success
cT = s.insertRedirectRule(ctx, tq, "1", "0", cT, true)
res := s.getVersioningRules(ctx, tq)
rulesMap := mkRedirectRulesMap(res.GetCompatibleRedirectRules())
s.Contains(rulesMap, "1")
s.Equal(rulesMap["1"], "0")
// failure due to cycle
s.insertRedirectRule(ctx, tq, "0", "1", cT, false)
s.Equal(res, s.getVersioningRules(ctx, tq))
// success with same conflict token but no cycle
s.insertRedirectRule(ctx, tq, "0", "2", cT, true)
}
func (s *VersioningIntegSuite) TestRedirectRuleReplace() {
// setup
ctx := testcore.NewContext()
tq := "test-redirect-rule-replace"
// get initial conflict token + do initial insert
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
cT = s.insertRedirectRule(ctx, tq, "1", "0", cT, true)
// success
cT = s.replaceRedirectRule(ctx, tq, "1", "2", cT, true)
res := s.getVersioningRules(ctx, tq)
rulesMap := mkRedirectRulesMap(res.GetCompatibleRedirectRules())
s.Contains(rulesMap, "1")
s.Equal(rulesMap["1"], "2")
// failure due to source not found
s.replaceRedirectRule(ctx, tq, "10", "3", cT, false)
s.Equal(res, s.getVersioningRules(ctx, tq))
// success with same conflict token and correct source
s.replaceRedirectRule(ctx, tq, "1", "3", cT, true)
}
func (s *VersioningIntegSuite) TestRedirectRuleDelete() {
// setup
ctx := testcore.NewContext()
tq := "test-redirect-rule-delete"
// get initial conflict token + do initial inserts
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
cT = s.insertRedirectRule(ctx, tq, "1", "0", cT, true)
cT = s.insertRedirectRule(ctx, tq, "2", "0", cT, true)
// success
cT = s.deleteRedirectRule(ctx, tq, "1", cT, true)
res := s.getVersioningRules(ctx, tq)
s.Equal(1, len(res.GetCompatibleRedirectRules()))
// failure due to source not found
s.deleteRedirectRule(ctx, tq, "1", cT, false)
s.Equal(res, s.getVersioningRules(ctx, tq))
// success with same conflict token and valid source
s.deleteRedirectRule(ctx, tq, "2", cT, true)
}
func (s *VersioningIntegSuite) TestCommitBuildID() {
// setup
ctx := testcore.NewContext()
tq := "test-commit-build-id"
// get initial conflict token
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
// no recent poller --> failure
s.commitBuildId(ctx, tq, "1", false, cT, false)
// no recent poller + force --> success
cT = s.commitBuildId(ctx, tq, "1", true, cT, true)
res := s.getVersioningRules(ctx, tq)
s.Equal(1, len(res.GetAssignmentRules()))
s.Equal(0, len(res.GetCompatibleRedirectRules()))
s.Equal("1", res.GetAssignmentRules()[0].GetRule().GetTargetBuildId())
s.Equal(float32(100), res.GetAssignmentRules()[0].GetRule().GetPercentageRamp().GetRampPercentage())
// recent versioned poller on wrong build ID --> failure
s.registerWorkflowAndPollVersionedTaskQueue(tq, "3", true)
s.commitBuildId(ctx, tq, "2", false, cT, false)
// recent unversioned poller on build ID 2 --> failure
s.registerWorkflowAndPollVersionedTaskQueue(tq, "2", false)
s.commitBuildId(ctx, tq, "2", false, cT, false)
// recent versioned poller on build ID 2 --> success
s.registerWorkflowAndPollVersionedTaskQueue(tq, "2", true)
s.commitBuildId(ctx, tq, "2", false, cT, true)
res = s.getVersioningRules(ctx, tq)
s.Equal(1, len(res.GetAssignmentRules()))
s.Equal(0, len(res.GetCompatibleRedirectRules()))
s.Equal("2", res.GetAssignmentRules()[0].GetRule().GetTargetBuildId())
s.Equal(float32(100), res.GetAssignmentRules()[0].GetRule().GetPercentageRamp().GetRampPercentage())
}
func mkRedirectRulesMap(redirectRules []*taskqueuepb.TimestampedCompatibleBuildIdRedirectRule) map[string]string {
ret := make(map[string]string)
for _, r := range redirectRules {
rule := r.GetRule()
ret[rule.GetSourceBuildId()] = rule.GetTargetBuildId()
}
return ret
}
func (s *VersioningIntegSuite) TestBasicVersionUpdate() {
ctx := testcore.NewContext()
tq := "functional-versioning-basic"
foo := s.prefixed("foo")
s.addNewDefaultBuildId(ctx, tq, foo)
res2, err := s.FrontendClient().GetWorkerBuildIdCompatibility(ctx, &workflowservice.GetWorkerBuildIdCompatibilityRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
})
s.NoError(err)
s.NotNil(res2)
s.Equal(foo, getCurrentDefault(res2))
}
func (s *VersioningIntegSuite) TestSeriesOfUpdates() {
ctx := testcore.NewContext()
tq := "functional-versioning-series"
for i := 0; i < 10; i++ {
s.addNewDefaultBuildId(ctx, tq, s.prefixed(fmt.Sprintf("foo-%d", i)))
}
s.addCompatibleBuildId(ctx, tq, s.prefixed("foo-2.1"), s.prefixed("foo-2"), false)
res, err := s.FrontendClient().GetWorkerBuildIdCompatibility(ctx, &workflowservice.GetWorkerBuildIdCompatibilityRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
})
s.NoError(err)
s.NotNil(res)
s.Equal(s.prefixed("foo-9"), getCurrentDefault(res))
s.Equal(s.prefixed("foo-2.1"), res.GetMajorVersionSets()[2].GetBuildIds()[1])
s.Equal(s.prefixed("foo-2"), res.GetMajorVersionSets()[2].GetBuildIds()[0])
}
func (s *VersioningIntegSuite) TestLinkToNonexistentCompatibleVersionReturnsNotFound() {
ctx := testcore.NewContext()
tq := "functional-versioning-compat-not-found"
res, err := s.FrontendClient().UpdateWorkerBuildIdCompatibility(ctx, &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
Operation: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewCompatibleBuildId{
AddNewCompatibleBuildId: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewCompatibleVersion{
NewBuildId: "foo",
ExistingCompatibleBuildId: "i don't exist yo",
},
},
})
s.Error(err)
s.Nil(res)
s.IsType(&serviceerror.NotFound{}, err)
}
func (s *VersioningIntegSuite) TestVersioningStatePersistsAcrossUnload() {
ctx := testcore.NewContext()
tq := "functional-versioning-persists"
s.addNewDefaultBuildId(ctx, tq, s.prefixed("foo"))
// Unload task queue to make sure the data is there when we load it again.
s.unloadTaskQueue(ctx, tq)
res, err := s.FrontendClient().GetWorkerBuildIdCompatibility(ctx, &workflowservice.GetWorkerBuildIdCompatibilityRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
})
s.NoError(err)
s.NotNil(res)
s.Equal(s.prefixed("foo"), getCurrentDefault(res))
}
func (s *VersioningIntegSuite) TestVersioningChangesPropagate() {
ctx := testcore.NewContext()
tq := "functional-versioning-propagate"
// ensure at least two hops
const partCount = 1 + partitionTreeDegree + partitionTreeDegree*partitionTreeDegree
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, partCount)
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, partCount)
for _, buildId := range []string{"foo", "foo-v2", "foo-v3"} {
s.addNewDefaultBuildId(ctx, tq, buildId)
s.waitForVersionSetPropagation(ctx, tq, buildId)
}
}
func (s *VersioningIntegSuite) TestMaxTaskQueuesPerBuildIdEnforced() {
ctx := testcore.NewContext()
buildId := fmt.Sprintf("b-%s", s.T().Name())
// Map a 3 task queues to this build ID and verify success
for i := 1; i <= 3; i++ {
taskQueue := fmt.Sprintf("q-%s-%d", s.T().Name(), i)
_, err := s.FrontendClient().UpdateWorkerBuildIdCompatibility(ctx, &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{
Namespace: s.Namespace().String(),
TaskQueue: taskQueue,
Operation: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewBuildIdInNewDefaultSet{
AddNewBuildIdInNewDefaultSet: buildId,
},
})
s.NoError(err)
}
// Map a fourth task queue to this build ID and verify it errors
taskQueue := fmt.Sprintf("q-%s-%d", s.T().Name(), 4)
_, err := s.FrontendClient().UpdateWorkerBuildIdCompatibility(ctx, &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{
Namespace: s.Namespace().String(),
TaskQueue: taskQueue,
Operation: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewBuildIdInNewDefaultSet{
AddNewBuildIdInNewDefaultSet: buildId,
},
})
var failedPreconditionError *serviceerror.FailedPrecondition
s.ErrorAs(err, &failedPreconditionError)
s.Equal("Exceeded max task queues allowed to be mapped to a single build ID: 3", failedPreconditionError.Message)
}
// func (s *VersioningIntegSuite) testWithMatchingBehavior(subtest func()) {
// for _, forceForward := range []bool{false, true} {
// for _, forceAsync := range []bool{false, true} {
// name := "NoForward"
// if forceForward {
// // force two levels of forwarding
// name = "ForceForward"
// }
// if forceAsync {
// name += "ForceAsync"
// } else {
// name += "AllowSync"
// }
//
// s.Run(name, func() {
// if forceForward {
// s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 13)
// s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 13)
// s.OverrideDynamicConfig(dynamicconfig.TestMatchingLBForceReadPartition, 5)
// s.OverrideDynamicConfig(dynamicconfig.TestMatchingLBForceWritePartition, 11)
// } else {
// s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1)
// s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1)
// }
// if forceAsync {
// s.OverrideDynamicConfig(dynamicconfig.TestMatchingDisableSyncMatch, true)
// } else {
// s.OverrideDynamicConfig(dynamicconfig.TestMatchingDisableSyncMatch, false)
// }
//
// subtest()
// })
// }
// }
// }
func (s *VersioningIntegSuite) TestDispatchNewWorkflowOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchNewWorkflow(false) })
}
func (s *VersioningIntegSuite) TestDispatchNewWorkflow() {
s.RunTestWithMatchingBehavior(func() { s.dispatchNewWorkflow(true) })
}
func (s *VersioningIntegSuite) dispatchNewWorkflow(newVersioning bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
wf := func(ctx workflow.Context) (string, error) {
return "done!", nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflow(wf)
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, newVersioning, v1, "", nil)
if newVersioning {
s.validateWorkflowEventsVersionStamps(ctx, run.GetID(), run.GetRunID(), []string{v1}, "")
} else {
s.validateWorkflowEventsVersionStamps(ctx, run.GetID(), run.GetRunID(), []string{}, "")
}
}
func (s *VersioningIntegSuite) TestDispatchNewWorkflowWithRamp() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
wf1 := func(ctx workflow.Context) (string, error) {
return "done v1!", nil
}
wf2 := func(ctx workflow.Context) (string, error) {
return "done v2!", nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rule := s.addAssignmentRule(ctx, tq, v1)
rule2 := s.addAssignmentRuleWithRamp(ctx, tq, v2, 50)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
s.waitForAssignmentRulePropagation(ctx, tq, rule2)
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2.Start())
defer w2.Stop()
counter := make(map[string]int)
for i := 0; i < 50; i++ {
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
var out string
s.NoError(run.Get(ctx, &out))
counter[out]++
}
// both builds should've got executions
s.Greater(counter["done v1!"], 0)
s.Greater(counter["done v2!"], 0)
s.Equal(50, counter["done v1!"]+counter["done v2!"])
}
func (s *VersioningIntegSuite) TestWorkflowStaysInBuildId() {
s.RunTestWithMatchingBehavior(s.workflowStaysInBuildId)
}
func (s *VersioningIntegSuite) workflowStaysInBuildId() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
act1Done := make(chan struct{})
rulesUpdated := make(chan struct{})
act1 := func() (string, error) {
close(act1Done)
return "act1 done!", nil
}
act2 := func() (string, error) {
s.WaitForChannel(ctx, rulesUpdated)
return "act2 done!", nil
}
wf := func(ctx workflow.Context) (string, error) {
var ret string
err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
DisableEagerExecution: true,
StartToCloseTimeout: 1 * time.Second,
}), act1).Get(ctx, &ret)
s.NoError(err)
s.Equal("act1 done!", ret)
// assignment rules change in here
err = workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
DisableEagerExecution: true,
StartToCloseTimeout: 1 * time.Second,
}), act2).Get(ctx, &ret)
s.NoError(err)
s.Equal("act2 done!", ret)
return "done!", nil
}
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflow(wf)
w1.RegisterActivity(act1)
w1.RegisterActivity(act2)
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
s.WaitForChannel(ctx, act1Done)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, true, v1, "", nil)
// update rules with v2 as the default build
rule = s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID())
s.NoError(err)
s.Equal(1, len(dw.GetPendingActivities()))
s.NotNil(dw.GetPendingActivities()[0].GetUseWorkflowBuildId())
close(rulesUpdated)
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, true, v1, "", nil)
s.validateWorkflowEventsVersionStamps(ctx, run.GetID(), run.GetRunID(), []string{
v1, // WFT
v1, // activity
// v1, skipped because it belongs to sticky queue
v1, // activity
// v1, skipped because it belongs to sticky queue
}, "")
}
func (s *VersioningIntegSuite) TestUnversionedWorkflowStaysUnversioned() {
s.RunTestWithMatchingBehavior(s.unversionedWorkflowStaysUnversioned)
}
func (s *VersioningIntegSuite) unversionedWorkflowStaysUnversioned() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
act1Done := make(chan struct{})
rulesUpdated := make(chan struct{})
act1 := func() (string, error) {
close(act1Done)
return "act1 done!", nil
}
act2 := func() (string, error) {
s.WaitForChannel(ctx, rulesUpdated)
return "act2 done!", nil
}
wf := func(ctx workflow.Context) (string, error) {
var ret string
err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
DisableEagerExecution: true,
StartToCloseTimeout: 1 * time.Second,
}), act1).Get(ctx, &ret)
s.NoError(err)
s.Equal("act1 done!", ret)
// assignment rules change in here
err = workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
DisableEagerExecution: true,
StartToCloseTimeout: 1 * time.Second,
}), act2).Get(ctx, &ret)
s.NoError(err)
s.Equal("act2 done!", ret)
return "done!", nil
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflow(wf)
w1.RegisterActivity(act1)
w1.RegisterActivity(act2)
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
s.WaitForChannel(ctx, act1Done)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), "", true, "binary-checksum", "", nil)
// update rules with v1 as the default build
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID())
s.NoError(err)
s.Equal(1, len(dw.GetPendingActivities()))
s.Nil(dw.GetPendingActivities()[0].GetAssignedBuildId())
close(rulesUpdated)
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), "", true, "binary-checksum", "", nil)
s.validateWorkflowEventsVersionStamps(ctx, run.GetID(), run.GetRunID(), []string{"", "", "", "", ""}, "")
}
func (s *VersioningIntegSuite) TestFirstWorkflowTaskAssignment_Spooled() {
s.RunTestWithMatchingBehavior(s.firstWorkflowTaskAssignmentSpooled)
}
func (s *VersioningIntegSuite) firstWorkflowTaskAssignmentSpooled() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
v3 := s.prefixed("v3")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
// start wf without worker
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
TaskQueue: tq,
// setting to a short timeout to verify first wf task timeout assignment
WorkflowTaskTimeout: 1 * time.Second,
}, "wf")
s.NoError(err)
// MS should have the correct build ID
s.waitForWorkflowBuildId(ctx, run.GetID(), run.GetRunID(), v1)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, true, "", "", nil)
// update latest build to v2
rule = s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
failedTask := make(chan struct{})
wf1 := func(ctx workflow.Context) (string, error) {
close(failedTask)
panic("failing WF task intentionally")
}
// run worker on v1 so it can fail the scheduled task
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
s.WaitForChannel(ctx, failedTask)
// After scheduling the second time, now MS should be assigned to v2
s.waitForWorkflowBuildId(ctx, run.GetID(), run.GetRunID(), v2)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v2, true, "", "", []string{v1})
// update latest build to v3
rule = s.addAssignmentRule(ctx, tq, v3)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
timedoutTask := make(chan struct{})
wf2 := func(ctx workflow.Context) (string, error) {
time.Sleep(1 * time.Second) //nolint:forbidigo
timedoutTask <- struct{}{}
time.Sleep(100 * time.Second) //nolint:forbidigo
return "return after long sleep", nil
}
// run worker on v2 so it can timeout the scheduled task
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
// since the WF cache is shared by all workers in this process, we need to set this in a way that ensures
// WFTs will timeout in w2 but not in w3
DeadlockDetectionTimeout: 1500 * time.Millisecond,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2.Start())
defer w2.Stop()
s.WaitForChannel(ctx, timedoutTask)
// After scheduling the third time, now MS should be assigned to v3
s.waitForWorkflowBuildId(ctx, run.GetID(), run.GetRunID(), v3)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v3, true, "", "", []string{v1, v2})
wf3 := func(ctx workflow.Context) (string, error) {
return "done on v3!", nil
}
// run worker on v3 so it can complete the wf
w3 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v3,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w3.RegisterWorkflowWithOptions(wf3, workflow.RegisterOptions{Name: "wf"})
s.NoError(w3.Start())
defer w3.Stop()
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done on v3!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v3, true, v3, "", []string{v1, v2})
s.validateWorkflowEventsVersionStamps(
ctx, run.GetID(), run.GetRunID(), []string{
v1, // failed wf tasks
v2, // timed out wf tasks
v3, // succeeded wf task
}, "",
)
}
func (s *VersioningIntegSuite) TestFirstWorkflowTaskAssignment_SyncMatch() {
s.RunTestWithMatchingBehavior(s.firstWorkflowTaskAssignmentSyncMatch)
}
func (s *VersioningIntegSuite) firstWorkflowTaskAssignmentSyncMatch() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
v3 := s.prefixed("v3")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
// v1 fails the task
failedTask := make(chan struct{})
wf1 := func(ctx workflow.Context) (string, error) {
failedTask <- struct{}{}
panic("failing WF task intentionally")
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
TaskQueue: tq,
// setting to a short timeout to verify first wf task timeout assignment
WorkflowTaskTimeout: 1 * time.Second,
}, "wf")
s.NoError(err)
// wait for two failures to make sure more attempts does not generate more history tasks
s.WaitForChannel(ctx, failedTask)
s.WaitForChannel(ctx, failedTask)
// MS should have the correct build ID
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, true, "", "", nil)
// v2 times out the task
timedoutTask := make(chan struct{})
wf2 := func(ctx workflow.Context) (string, error) {
time.Sleep(1 * time.Second) //nolint:forbidigo
timedoutTask <- struct{}{}
time.Sleep(100 * time.Second) //nolint:forbidigo
return "return after long sleep", nil
}
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
// since the WF cache is shared by all workers in this process, we need to set this in a way that ensures
// WFTs will timeout in w2 but not in w3
DeadlockDetectionTimeout: 1500 * time.Millisecond,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2.Start())
defer w2.Stop()
// update latest build to v2
rule = s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
// wait for multiple timeouts to make sure more attempts do not generate more history events
s.WaitForChannel(ctx, timedoutTask)
s.WaitForChannel(ctx, timedoutTask)
s.WaitForChannel(ctx, timedoutTask)
// After scheduling the second time, now MS should be assigned to v2
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v2, true, "", "", []string{v1})
// v3 can process the task
wf3 := func(ctx workflow.Context) (string, error) {
return "done on v3!", nil
}
// run worker on v3 so it can complete the wf
w3 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v3,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w3.RegisterWorkflowWithOptions(wf3, workflow.RegisterOptions{Name: "wf"})
s.NoError(w3.Start())
defer w3.Stop()
// update latest build to v3
rule = s.addAssignmentRule(ctx, tq, v3)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done on v3!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v3, true, v3, "", []string{v1, v2})
s.validateWorkflowEventsVersionStamps(
ctx, run.GetID(), run.GetRunID(), []string{
v1, // failed wf tasks
v2, // timed out wf tasks
v3, // succeeded wf task
}, "",
)
}
func (s *VersioningIntegSuite) TestIndependentActivityTaskAssignment_Spooled_VersionedWorkflow() {
s.RunTestWithMatchingBehavior(func() { s.independentActivityTaskAssignmentSpooled(true) })
}
func (s *VersioningIntegSuite) TestIndependentActivityTaskAssignment_Spooled_UnversionedWorkflow() {
s.RunTestWithMatchingBehavior(func() { s.independentActivityTaskAssignmentSpooled(false) })
}
func (s *VersioningIntegSuite) independentActivityTaskAssignmentSpooled(versionedWf bool) {
wfTq := testcore.RandomizeStr(s.T().Name())
actTq := testcore.RandomizeStr(s.T().Name())
wfV1 := s.prefixed("wfv1")
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
v3 := s.prefixed("v3")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rule := s.addAssignmentRule(ctx, actTq, v1)
s.waitForAssignmentRulePropagation(ctx, actTq, rule)
if versionedWf {
rule = s.addAssignmentRule(ctx, wfTq, wfV1)
s.waitForAssignmentRulePropagation(ctx, wfTq, rule)
}
wf := func(ctx workflow.Context) (string, error) {
var ret string
err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
TaskQueue: actTq,
StartToCloseTimeout: 1 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: 1 * time.Second,
BackoffCoefficient: 1,
},
}), "act").Get(ctx, &ret)
s.NoError(err)
s.Equal("done in v3!", ret)
return "done!", nil
}
// start workflow worker
wfw := worker.New(s.sdkClient, wfTq, worker.Options{
BuildID: wfV1,
UseBuildIDForVersioning: versionedWf,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
wfw.RegisterWorkflow(wf)
s.NoError(wfw.Start())
defer wfw.Stop()
// start wf without activity worker
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
TaskQueue: wfTq,
// setting to a short timeout to verify first wf task timeout assignment
WorkflowTaskTimeout: 1 * time.Second,
}, wf)
s.NoError(err)
// MS should have the correct build ID after finishing the first WFT
s.Eventually(
func() bool {
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID())
s.NoError(err)
if len(dw.GetPendingActivities()) == 0 {
return false
}
if versionedWf {
s.Equal(wfV1, dw.GetWorkflowExecutionInfo().GetAssignedBuildId())
s.Equal(wfV1, dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetBuildId())
} else {
s.Equal("", dw.GetWorkflowExecutionInfo().GetAssignedBuildId())
s.False(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetUseVersioning())
}
return v1 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId()
},
10*time.Second,
50*time.Millisecond,
)
// update latest build to v2
rule = s.addAssignmentRule(ctx, actTq, v2)
s.waitForAssignmentRulePropagation(ctx, actTq, rule)
failedTask := make(chan struct{})
act1 := func() (string, error) {
close(failedTask)
return "", errors.New("failing activity task intentionally") //nolint:err113
}
// run v1 activity worker so it can fail the scheduled activity
w1 := worker.New(s.sdkClient, actTq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterActivityWithOptions(act1, activity.RegisterOptions{Name: "act"})
s.NoError(w1.Start())
defer w1.Stop()
s.WaitForChannel(ctx, failedTask)
// After scheduling the second time, now pending activity should be assigned to v2
s.Eventually(
func() bool {
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID())
s.NoError(err)
s.Equal(1, len(dw.GetPendingActivities()))
return v2 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId()
},
10*time.Second,
50*time.Millisecond,
)
// update latest build to v3
rule = s.addAssignmentRule(ctx, actTq, v3)
s.waitForAssignmentRulePropagation(ctx, actTq, rule)
timedoutTask := make(chan struct{})
act2 := func() (string, error) {
time.Sleep(1 * time.Second) //nolint:forbidigo
close(timedoutTask)
time.Sleep(100 * time.Second) //nolint:forbidigo
return "return after long sleep", nil
}
// run v2 activity worker so it can timeout the scheduled activity
w2 := worker.New(s.sdkClient, actTq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
// since the WF cache is shared by all workers in this process, we need to set this in a way that ensures
// WFTs will timeout in w2 but not in w3
DeadlockDetectionTimeout: 1500 * time.Millisecond,
})
w2.RegisterActivityWithOptions(act2, activity.RegisterOptions{Name: "act"})
s.NoError(w2.Start())
defer w2.Stop()
s.WaitForChannel(ctx, timedoutTask)
// After scheduling the third time, now pending activity should be assigned to v3
s.Eventually(
func() bool {
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID())
s.NoError(err)
s.Equal(1, len(dw.GetPendingActivities()))
return v3 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId()
},
10*time.Second,
50*time.Millisecond,
)
act3 := func() (string, error) {
return "done in v3!", nil
}
// run worker on v3 so it can complete the wf
w3 := worker.New(s.sdkClient, actTq, worker.Options{
BuildID: v3,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w3.RegisterActivityWithOptions(act3, activity.RegisterOptions{Name: "act"})
s.NoError(w3.Start())
defer w3.Stop()
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
if versionedWf {
s.validateWorkflowEventsVersionStamps(
ctx, run.GetID(), run.GetRunID(), []string{
wfV1,
v3, // succeeded activity
// wfV1, removed because it's on a sticky queue
}, "",
)
} else {
s.validateWorkflowEventsVersionStamps(
ctx, run.GetID(), run.GetRunID(), []string{
"",
v3, // succeeded activity
"",
}, "",
)
}
}
func (s *VersioningIntegSuite) TestIndependentActivityTaskAssignment_SyncMatch_VersionedWorkflow() {
s.RunTestWithMatchingBehavior(func() { s.independentActivityTaskAssignmentSyncMatch(true) })
}
func (s *VersioningIntegSuite) TestIndependentActivityTaskAssignment_SyncMatch_UnversionedWorkflow() {
s.RunTestWithMatchingBehavior(func() { s.independentActivityTaskAssignmentSyncMatch(false) })
}
func (s *VersioningIntegSuite) independentActivityTaskAssignmentSyncMatch(versionedWf bool) {
wfTq := testcore.RandomizeStr(s.T().Name())
actTq := testcore.RandomizeStr(s.T().Name())
wfV1 := s.prefixed("wfv1")
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
v3 := s.prefixed("v3")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rule := s.addAssignmentRule(ctx, actTq, v1)
s.waitForAssignmentRulePropagation(ctx, actTq, rule)
if versionedWf {
rule := s.addAssignmentRule(ctx, wfTq, wfV1)
s.waitForAssignmentRulePropagation(ctx, wfTq, rule)
}
wf := func(ctx workflow.Context) (string, error) {
var ret string
err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
TaskQueue: actTq,
StartToCloseTimeout: 1 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: 1 * time.Second,
BackoffCoefficient: 1,
},
}), "act").Get(ctx, &ret)
s.NoError(err)
s.Equal("done in v3!", ret)
return "done!", nil
}
// start workflow worker
wfw := worker.New(s.sdkClient, wfTq, worker.Options{
BuildID: wfV1,
UseBuildIDForVersioning: versionedWf,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
wfw.RegisterWorkflow(wf)
s.NoError(wfw.Start())
defer wfw.Stop()
// v1 fails the activity
failedTask := make(chan struct{})
act1 := func() (string, error) {
close(failedTask)
return "", errors.New("failing activity task intentionally")
}
w1 := worker.New(s.sdkClient, actTq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterActivityWithOptions(act1, activity.RegisterOptions{Name: "act"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
TaskQueue: wfTq,
// setting to a short timeout to verify first wf task timeout assignment
WorkflowTaskTimeout: 1 * time.Second,
}, wf)
s.NoError(err)
s.WaitForChannel(ctx, failedTask)
// MS should have the correct build ID after finishing the first WFT
s.Eventually(
func() bool {
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID())
s.NoError(err)
if len(dw.GetPendingActivities()) == 0 {
return false
}
if versionedWf {
s.Equal(wfV1, dw.GetWorkflowExecutionInfo().GetAssignedBuildId())
s.Equal(wfV1, dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetBuildId())
} else {
s.Equal("", dw.GetWorkflowExecutionInfo().GetAssignedBuildId())
s.False(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetUseVersioning())
}
return v1 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId()
},
10*time.Second,
50*time.Millisecond,
)
// v2 timesout the activity
timedoutTask := make(chan struct{})
act2 := func() (string, error) {
time.Sleep(1 * time.Second) //nolint:forbidigo
close(timedoutTask)
time.Sleep(100 * time.Second) //nolint:forbidigo
return "return after long sleep", nil
}
w2 := worker.New(s.sdkClient, actTq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
// since the WF cache is shared by all workers in this process, we need to set this in a way that ensures
// WFTs will timeout in w2 but not in w3
DeadlockDetectionTimeout: 1500 * time.Millisecond,
})
w2.RegisterActivityWithOptions(act2, activity.RegisterOptions{Name: "act"})
s.NoError(w2.Start())
defer w2.Stop()
// update latest build to v2
rule = s.addAssignmentRule(ctx, actTq, v2)
s.waitForAssignmentRulePropagation(ctx, actTq, rule)
s.WaitForChannel(ctx, timedoutTask)
// After scheduling the second time, now pending activity should be assigned to v2
s.Eventually(
func() bool {
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID())
s.NoError(err)
s.Equal(1, len(dw.GetPendingActivities()))
return v2 == dw.GetPendingActivities()[0].GetLastIndependentlyAssignedBuildId()
},
10*time.Second,
50*time.Millisecond,
)
// v3 can process the activity
act3 := func() (string, error) {
return "done in v3!", nil
}
// run worker on v3 so it can complete the wf
w3 := worker.New(s.sdkClient, actTq, worker.Options{
BuildID: v3,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w3.RegisterActivityWithOptions(act3, activity.RegisterOptions{Name: "act"})
s.NoError(w3.Start())
defer w3.Stop()
// update latest build to v3
rule = s.addAssignmentRule(ctx, actTq, v3)
s.waitForAssignmentRulePropagation(ctx, actTq, rule)
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
if versionedWf {
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), wfV1, true, wfV1, "", nil)
s.validateWorkflowEventsVersionStamps(
ctx, run.GetID(), run.GetRunID(), []string{
wfV1,
v3, // succeeded activity
// wfV1, skipping stamp because this is a sticky queue task
}, "",
)
} else {
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), "", true, wfV1, "", nil)
s.validateWorkflowEventsVersionStamps(
ctx, run.GetID(), run.GetRunID(), []string{
"",
v3, // succeeded activity
"",
}, "",
)
}
}
func (s *VersioningIntegSuite) TestWorkflowTaskRedirectInRetryFirstTask() {
s.RunTestWithMatchingBehavior(func() { s.testWorkflowTaskRedirectInRetry(true) })
}
func (s *VersioningIntegSuite) TestWorkflowTaskRedirectInRetryNonFirstTask() {
s.RunTestWithMatchingBehavior(func() { s.testWorkflowTaskRedirectInRetry(false) })
}
func (s *VersioningIntegSuite) testWorkflowTaskRedirectInRetry(firstTask bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v1.1")
v12 := s.prefixed("v1.2")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
act := func() error {
return nil
}
// v1 fails the task
failedTask := make(chan struct{})
wf1 := func(ctx workflow.Context) (string, error) {
if !firstTask {
// add an activity to move workflow past first WFT
var out interface{}
err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 1 * time.Second}), act).Get(ctx, &out)
s.NoError(err)
}
failedTask <- struct{}{}
panic("failing WF task intentionally")
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
w1.RegisterActivity(act)
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
TaskQueue: tq,
// setting to a short timeout to verify first wf task timeout assignment
WorkflowTaskTimeout: 1 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
BackoffCoefficient: 1.,
InitialInterval: time.Second,
},
}, "wf")
s.NoError(err)
// wait for multiple failures to make sure more attempts does not generate more history tasks
s.WaitForChannel(ctx, failedTask)
s.WaitForChannel(ctx, failedTask)
s.WaitForChannel(ctx, failedTask)
expectedStampBuildId := ""
if !firstTask {
expectedStampBuildId = v1
}
// MS should have the correct build ID
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, true, expectedStampBuildId, "", nil)
// v11 times out the task
timedoutTask := make(chan struct{})
wf11 := func(ctx workflow.Context) (string, error) {
if !firstTask {
// add an activity to move workflow past first WFT
var out interface{}
err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 1 * time.Second}), act).Get(ctx, &out)
s.NoError(err)
}
time.Sleep(1 * time.Second) //nolint:forbidigo
timedoutTask <- struct{}{}
time.Sleep(100 * time.Second) //nolint:forbidigo
return "return after long sleep", nil
}
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
// since the WF cache is shared by all workers in this process, we need to set this in a way that ensures
// WFTs will timeout in w2 but not in w3
DeadlockDetectionTimeout: 1500 * time.Millisecond,
})
w11.RegisterWorkflowWithOptions(wf11, workflow.RegisterOptions{Name: "wf"})
w11.RegisterActivity(act)
s.NoError(w11.Start())
defer w11.Stop()
rule2 := s.addRedirectRule(ctx, tq, v1, v11)
s.waitForRedirectRulePropagation(ctx, tq, rule2)
// wait for multiple timeouts to make sure more attempts does not generate more history tasks
s.WaitForChannel(ctx, timedoutTask)
s.WaitForChannel(ctx, timedoutTask)
s.WaitForChannel(ctx, timedoutTask)
// After scheduling the second time, now MS should be assigned to v2
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, true, expectedStampBuildId, "", []string{v1})
// v12 can process the task
wf12 := func(ctx workflow.Context) (string, error) {
if !firstTask {
// add an activity to move workflow past first WFT
var out interface{}
err := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 1 * time.Second}), act).Get(ctx, &out)
s.NoError(err)
}
return "done on v1.2!", nil
}
// run worker on v12 so it can complete the wf
w12 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v12,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w12.RegisterWorkflowWithOptions(wf12, workflow.RegisterOptions{Name: "wf"})
w12.RegisterActivity(act)
s.NoError(w12.Start())
defer w12.Stop()
rule2 = s.addRedirectRule(ctx, tq, v11, v12)
s.waitForRedirectRulePropagation(ctx, tq, rule2)
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done on v1.2!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v12, true, v12, "", []string{v1, v11})
expectedStamps := []string{
v1, // failed wf task
v11, // timed out wf task show up in history because a redirect happened after them
v12, // succeeded wf task
}
if !firstTask {
expectedStamps = []string{
v1, // first wf task
v1, // activity task
// v1, // skipping stamp for failed wf task on sticky queue
v1, // failed wf task on normal queue
v11, // timed out wf task show up in history because they happened on a different build ID
v12, // succeeded wf task
}
}
s.validateWorkflowEventsVersionStamps(ctx, run.GetID(), run.GetRunID(), expectedStamps, "")
}
func (s *VersioningIntegSuite) TestDispatchNotUsingVersioningOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchNotUsingVersioning(false) })
}
func (s *VersioningIntegSuite) TestDispatchNotUsingVersioning() {
s.RunTestWithMatchingBehavior(func() { s.dispatchNotUsingVersioning(true) })
}
func (s *VersioningIntegSuite) dispatchNotUsingVersioning(newVersioning bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
wf1nover := func(ctx workflow.Context) (string, error) {
return "done without versioning!", nil
}
wf1 := func(ctx workflow.Context) (string, error) {
return "done with versioning!", nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1nover := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: false,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1nover.RegisterWorkflowWithOptions(wf1nover, workflow.RegisterOptions{Name: "wf"})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1nover.Start())
defer w1nover.Stop()
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done with versioning!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, newVersioning, v1, "", nil)
}
func (s *VersioningIntegSuite) TestDispatchNewWorkflowStartWorkerFirst() {
s.RunTestWithMatchingBehavior(s.dispatchNewWorkflowStartWorkerFirst)
}
func (s *VersioningIntegSuite) dispatchNewWorkflowStartWorkerFirst() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
wf := func(ctx workflow.Context) (string, error) {
return "done!", nil
}
// run worker before registering build. it will use guessed set id
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflow(wf)
s.NoError(w1.Start())
defer w1.Stop()
// wait for it to start polling
time.Sleep(200 * time.Millisecond) //nolint:forbidigo
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, false, v1, "", nil)
}
func (s *VersioningIntegSuite) TestDispatchUnversionedRemainsUnversioned() {
s.RunTestWithMatchingBehavior(s.dispatchUnversionedRemainsUnversioned)
}
func (s *VersioningIntegSuite) dispatchUnversionedRemainsUnversioned() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
started := make(chan struct{}, 1)
wf := func(ctx workflow.Context) (string, error) {
started <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
return "done!", nil
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
// no build ID
})
w1.RegisterWorkflow(wf)
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
s.WaitForChannel(ctx, started)
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
// unblock the workflow
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), run.GetRunID(), "wait", nil))
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), "", false, "binary-checksum", "", nil)
}
func (s *VersioningIntegSuite) TestDispatchUpgradeStopOldOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchUpgrade(false, true) })
}
func (s *VersioningIntegSuite) TestDispatchUpgradeWaitOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchUpgrade(false, false) })
}
func (s *VersioningIntegSuite) TestDispatchUpgradeStopOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchUpgrade(true, true) })
}
func (s *VersioningIntegSuite) TestDispatchUpgradeWait() {
s.RunTestWithMatchingBehavior(func() { s.dispatchUpgrade(true, false) })
}
func (s *VersioningIntegSuite) dispatchUpgrade(newVersioning, stopOld bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v11")
started := make(chan struct{}, 1)
wf1 := func(ctx workflow.Context) (string, error) {
started <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
return "done!", nil
}
wf11 := func(ctx workflow.Context) (string, error) {
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
return "done from 1.1!", nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
s.WaitForChannel(ctx, started)
// now add v11 as compatible so the next workflow task runs there
if newVersioning {
rule := s.addRedirectRule(ctx, tq, v1, v11)
s.waitForRedirectRulePropagation(ctx, tq, rule)
} else {
s.addCompatibleBuildId(ctx, tq, v11, v1, false)
s.waitForVersionSetPropagation(ctx, tq, v11)
}
// add another 100ms to make sure it got to sticky queues also
time.Sleep(100 * time.Millisecond) //nolint:forbidigo
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w11.RegisterWorkflowWithOptions(wf11, workflow.RegisterOptions{Name: "wf"})
s.NoError(w11.Start())
defer w11.Stop()
// Two cases:
if stopOld {
// Stop the old worker. Workflow tasks will go to the sticky queue, which will see that
// it's not the latest and kick them back to the normal queue, which will be dispatched
// to v11.
w1.Stop()
} else {
// Don't stop the old worker.
if !newVersioning {
// In this case, w1 will still have some pollers blocked on
// the normal queue which could pick up tasks that we want to go to v11. (We don't
// interrupt long polls.) To ensure those polls don't interfere, wait for them to
// expire.
time.Sleep(longPollTime) //nolint:forbidigo
}
}
// unblock the workflow
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), run.GetRunID(), "wait", nil))
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done from 1.1!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, newVersioning, v11, "", []string{v1})
}
type activityFailMode int
const (
dontFailActivity = iota
failActivity
timeoutActivity
)
func (s *VersioningIntegSuite) TestDispatchActivityOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchActivity(dontFailActivity, false, false) })
}
func (s *VersioningIntegSuite) TestDispatchActivityFailOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchActivity(failActivity, false, false) })
}
func (s *VersioningIntegSuite) TestDispatchActivityTimeoutOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchActivity(timeoutActivity, false, false) })
}
func (s *VersioningIntegSuite) TestDispatchActivity() {
s.RunTestWithMatchingBehavior(func() { s.dispatchActivity(dontFailActivity, true, false) })
}
func (s *VersioningIntegSuite) TestDispatchActivityFail() {
s.RunTestWithMatchingBehavior(func() { s.dispatchActivity(failActivity, true, false) })
}
func (s *VersioningIntegSuite) TestDispatchActivityTimeout() {
s.RunTestWithMatchingBehavior(func() { s.dispatchActivity(timeoutActivity, true, false) })
}
func (s *VersioningIntegSuite) TestDispatchActivityCrossTq() {
s.RunTestWithMatchingBehavior(func() { s.dispatchActivity(dontFailActivity, true, true) })
}
func (s *VersioningIntegSuite) TestDispatchActivityFailCrossTq() {
s.RunTestWithMatchingBehavior(func() { s.dispatchActivity(failActivity, true, true) })
}
func (s *VersioningIntegSuite) TestDispatchActivityTimeoutCrossTq() {
s.RunTestWithMatchingBehavior(func() { s.dispatchActivity(timeoutActivity, true, true) })
}
func (s *VersioningIntegSuite) dispatchActivity(failMode activityFailMode, newVersioning bool, crossTq bool) {
// This also implicitly tests that a workflow stays on a compatible version set if a new
// incompatible set is registered, because wf2 just panics. It further tests that
// stickiness on v1 is not broken by registering v2, because the channel send will panic on
// replay after we close the channel.
tq := testcore.RandomizeStr(s.T().Name())
actxTq := testcore.RandomizeStr(s.T().Name() + "activity")
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
started := make(chan struct{}, 1)
var act1state, act2state atomic.Int32
doAct := func(state *atomic.Int32, output string) (string, error) {
if state.Add(1) == 1 {
switch failMode {
case failActivity:
return "", errors.New("try again")
case timeoutActivity:
time.Sleep(5 * time.Second) //nolint:forbidigo
return "ignored", nil
}
}
return output, nil
}
act1 := func() (string, error) {
return doAct(&act1state, "v1")
}
act2 := func() (string, error) {
return doAct(&act2state, "v2")
}
act1xTq := func() (string, error) {
return doAct(&act1state, "v1xTq")
}
act2xTq := func() (string, error) {
return doAct(&act2state, "v2xTq")
}
wf1 := func(ctx workflow.Context) (string, error) {
started <- struct{}{}
// wait for signal
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
actTq := tq
if crossTq {
actTq = actxTq
}
// run two activities
fut1 := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: time.Minute,
DisableEagerExecution: true,
VersioningIntent: temporal.VersioningIntentCompatible,
StartToCloseTimeout: 1 * time.Second,
TaskQueue: actTq,
}), "act")
fut2 := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: time.Minute,
DisableEagerExecution: true,
VersioningIntent: temporal.VersioningIntentDefault, // this one should go to default
StartToCloseTimeout: 1 * time.Second,
TaskQueue: actTq,
}), "act")
var val1, val2 string
s.NoError(fut1.Get(ctx, &val1))
s.NoError(fut2.Get(ctx, &val2))
return val1 + val2, nil
}
wf2 := func(ctx workflow.Context) (string, error) {
panic("workflow should not run on v2")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
w1.RegisterActivityWithOptions(act1, activity.RegisterOptions{Name: "act"})
s.NoError(w1.Start())
defer w1.Stop()
w1xTq := worker.New(s.sdkClient, actxTq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1xTq.RegisterActivityWithOptions(act1xTq, activity.RegisterOptions{Name: "act"})
s.NoError(w1xTq.Start())
defer w1xTq.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started)
close(started) // force panic if replayed
// now register v2 as default
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
rule = s.addAssignmentRule(ctx, actxTq, v2)
s.waitForAssignmentRulePropagation(ctx, actxTq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v2)
s.waitForVersionSetPropagation(ctx, tq, v2)
}
// start worker for v2
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
w2.RegisterActivityWithOptions(act2, activity.RegisterOptions{Name: "act"})
s.NoError(w2.Start())
defer w2.Stop()
w2xTq := worker.New(s.sdkClient, actxTq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2xTq.RegisterActivityWithOptions(act2xTq, activity.RegisterOptions{Name: "act"})
s.NoError(w2xTq.Start())
defer w2xTq.Stop()
// unblock the workflow
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), run.GetRunID(), "wait", nil))
var out string
s.NoError(run.Get(ctx, &out))
if crossTq {
s.Equal("v1xTqv2xTq", out)
} else {
s.Equal("v1v2", out)
}
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, newVersioning, v1, "", nil)
}
func (s *VersioningIntegSuite) TestDispatchActivityUpgrade() {
// Testing:
// - redirecting dependent activity redirects wf
// - this also implicitly verifies that sticky queue is cleared after redirecting activity task
// - redirecting independent activity does not redirect wf
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v1.1")
v12 := s.prefixed("v1.2")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
startedWf := make(chan struct{}, 1)
proceedWf := make(chan struct{}, 1)
started11 := make(chan struct{}, 1)
proceed11 := make(chan struct{}, 1)
started12 := make(chan struct{}, 1)
proceed12 := make(chan struct{}, 1)
act1 := func() (string, error) {
return "v1", nil
}
act11 := func() (string, error) {
started11 <- struct{}{}
s.WaitForChannel(ctx, proceed11)
return "v1.1", nil
}
act12 := func() (string, error) {
started12 <- struct{}{}
s.WaitForChannel(ctx, proceed12)
return "v1.2", nil
}
wf := func(ctx workflow.Context) (string, error) {
startedWf <- struct{}{}
// Wait for adding v1 -> v1.1 redirect rule. We use a channel instead of WF signal to make sure the rules are
// added between WFT started and completed and next task seeing the updated rules is an activity, not a WFT.
<-proceedWf
var val1, val2 string
// run two activities
err1 := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: time.Minute,
DisableEagerExecution: true,
VersioningIntent: temporal.VersioningIntentCompatible,
StartToCloseTimeout: 1 * time.Second,
}), "act").Get(ctx, &val1)
s.NoError(err1)
startedWf <- struct{}{}
// Wait for adding v1.1 -> v1.2 redirect rule. We use a channel instead of WF signal to make sure the rules are
// added between WFT started and completed and next task seeing the updated rules is an activity, not a WFT.
<-proceedWf
err2 := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: time.Minute,
DisableEagerExecution: true,
VersioningIntent: temporal.VersioningIntentDefault,
StartToCloseTimeout: 1 * time.Second,
}), "act").Get(ctx, &val2)
s.NoError(err2)
return val1 + val2, nil
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
DeadlockDetectionTimeout: 5 * time.Second,
})
w1.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{Name: "wf"})
w1.RegisterActivityWithOptions(act1, activity.RegisterOptions{Name: "act"})
s.NoError(w1.Start())
defer w1.Stop()
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
DeadlockDetectionTimeout: 5 * time.Second,
})
w11.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{Name: "wf"})
w11.RegisterActivityWithOptions(act11, activity.RegisterOptions{Name: "act"})
s.NoError(w11.Start())
defer w11.Stop()
w12 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v12,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
DeadlockDetectionTimeout: 5 * time.Second,
})
w12.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{Name: "wf"})
w12.RegisterActivityWithOptions(act12, activity.RegisterOptions{Name: "act"})
s.NoError(w12.Start())
defer w12.Stop()
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, startedWf)
rule2 := s.addRedirectRule(ctx, tq, v1, v11)
s.waitForRedirectRulePropagation(ctx, tq, rule2)
proceedWf <- struct{}{}
s.WaitForChannel(ctx, started11)
// wf assigned build ID should be updated by activity redirect
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, true, v1, "", []string{v1})
// let activity finish
proceed11 <- struct{}{}
// wf replays on 1.1 so need to unblock it an extra time
s.WaitForChannel(ctx, startedWf)
proceedWf <- struct{}{}
s.WaitForChannel(ctx, startedWf)
rule2 = s.addRedirectRule(ctx, tq, v11, v12)
s.waitForRedirectRulePropagation(ctx, tq, rule2)
proceedWf <- struct{}{}
s.WaitForChannel(ctx, started12)
// wf assigned build ID should not be updated by independent activity redirect
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, true, v11, "", []string{v1})
// let activity finish
proceed12 <- struct{}{}
// wf replays on 1.2 so need to unblock it two extra times
s.WaitForChannel(ctx, startedWf)
proceedWf <- struct{}{}
s.WaitForChannel(ctx, startedWf)
proceedWf <- struct{}{}
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("v1.1v1.2", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v12, true, v12, "", []string{v1, v11})
}
func (s *VersioningIntegSuite) TestRedirectWithConcurrentActivities() {
// Testing that wf never "goes back" to older build ID in presence of concurrent activities and random failures.
//
// SETUP:
// 1- Run workers with build IDs v1.0 to v1.9.
// 2- Workflows runs a nested loop of 10 * `activityRuns`. First loop is per build ID, second is to run parallel
// activities for each build ID.
// 3- Activities fail by `activityErrorRate` probability. Otherwise, they return success after a random wait.
// 4- The first activity seeing a build ID (i.e. being run by that build ID) removes the redirect rule targeting
// that build ID to test the workflow behaves correctly in absence of the applied redirect rule.
// 5- One activity in each batch is responsible to add redirect rule to the next build ID. since, there is random
// delay in activities, more activities of the same batch likely start both before and after this activity.
// 6- Workflow waits for completion of all activities in a batch before going to the next one.
//
// VERIFYING:
// 1- After each activity is completed, in workflow we verify that it is not completed by a build ID newer of that
// of the workflow worker.
// 2- Workflow finishes. I.e. no task is dropped without rescheduling due to build ID mismatch.
// 3- At the end of the test we verify that the wf and activity started event build IDs are compatible with
// workflows output (which contains the build ID of each completed activity)
// 4- Redirect counter of all started events is valid
// 5- Redirect rules were applied at least to one activity.
// 6- At least one activity was retried.
// 7- Some history events are unordered based on event timestamp (due to parallel activity random delay)
// Reduce user data long poll time for faster propagation of the versioning data. This is needed because of the
// exponential minWaitTime logic in userDataManagerImpl that gets triggered because rules change very fast in
// this test.
s.OverrideDynamicConfig(dynamicconfig.MatchingGetUserDataLongPollTimeout, 2*time.Second)
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1.0")
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
versions := []string{v1}
for v := 1; v <= 9; v++ {
versions = append(versions, s.prefixed("v1."+strconv.Itoa(v)))
}
activityErrorRate := .2 // chance of each activity attempt fail
activityRuns := int32(10) // run the activity ~10 times on each version
activityCounter := atomic.Int32{}
triggerRedirectAtActivityRun := atomic.Int32{}
triggerRedirectAtActivityRun.Store(1)
lastRedirectTarget := atomic.Value{}
lastRedirectTarget.Store(versions[0] + " redirect cleaned")
var workers []worker.Worker
act := func(version string, runId int32) (string, error) {
runs := activityCounter.Add(1)
s.T().Logf("Starting activity %d on %s at %d\n", runId, version, runs)
if lastRedirectTarget.CompareAndSwap(version, version+" observed") && version != versions[0] {
// The last redirect rule is applied and observed by an activity, now delete it to make sure wf keeps using
// the right build ID after applying the redirect rule, even when the rule is not present anymore.
index, err := strconv.Atoi(version[len(version)-1:]) // get the last char of version is the index in the versions array
s.NoError(err)
s.T().Logf("Removing redirect from %s to %s \n", versions[index-1], version)
s.removeRedirectRule(ctx, tq, versions[index-1])
lastRedirectTarget.CompareAndSwap(version+" observed", version+" redirect cleaned")
}
if rand.Float64() < activityErrorRate {
return "", errors.New("intentionally failing activity")
}
if triggerRedirectAtActivityRun.Load() == runId {
// When enough activities are run using the current version, add redirect rule to the next version.
v := runId / activityRuns
if int(v+1) < len(versions) {
// wait for last redirect rule to be cleaned up
for !lastRedirectTarget.CompareAndSwap(versions[v]+" redirect cleaned", versions[v+1]) {
}
s.T().Logf("Adding redirect from %s to %s at %d\n", versions[v], versions[v+1], runs)
s.addRedirectRule(ctx, tq, versions[v], versions[v+1])
// Intentionally do not wait for propagation of the rules to partitions. Waiting will linger this
// activity and allows all the other concurrent activities to finish, leaving only the WFT task to
// see the redirect rule for the first time.
triggerRedirectAtActivityRun.CompareAndSwap(runId, runId+activityRuns)
}
}
// Add random sleep to simulate network delay
//nolint:forbidigo
time.Sleep(time.Duration(int64(rand.Intn(50)) * int64(time.Millisecond))) //nolint:forbidigo
s.T().Logf("Completing activity %d on %s at %d\n", runId, version, runs)
return version, nil
}
wf := func(wfCtx workflow.Context, wfVersion string) (string, error) {
var res []string
// because of rule propagation delay we run for more than 10 cycles to make sure all versions are seen
for i := 0; i <= 12; i++ {
var futures []workflow.Future
for j := 0; j < int(activityRuns); j++ {
f := workflow.ExecuteActivity(workflow.WithActivityOptions(
wfCtx, workflow.ActivityOptions{
DisableEagerExecution: true,
VersioningIntent: temporal.VersioningIntentCompatible,
StartToCloseTimeout: 200 * time.Millisecond,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: 10 * time.Millisecond,
BackoffCoefficient: 1,
},
}), "act", i*int(activityRuns)+j)
futures = append(futures, f)
}
for _, f := range futures {
var activityVersion string
err := f.Get(wfCtx, &activityVersion)
s.NoError(err)
res = append(res, activityVersion)
// The output of a newer build ID should never be sent to a wf worker of an older build ID
s.Assert().GreaterOrEqual(wfVersion, activityVersion)
// TODO: uncomment this check once workflow.GetInfo(wfCtx).GetCurrentBuildID() returns correct value
// based on last started task build ID, not last completed task build ID.
// s.Assert().GreaterOrEqual(workflow.GetInfo(wfCtx).GetCurrentBuildID(), activityVersion)
}
}
return strings.Join(res, " "), nil
}
// run all workers
for i := 0; i <= 9; i++ {
v := versions[i]
w := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
MaxConcurrentActivityTaskPollers: 2,
// Limit the number of concurrent activities so not all scheduled activities are immediately started.
MaxConcurrentActivityExecutionSize: 2,
})
w.RegisterWorkflowWithOptions(
func(ctx workflow.Context) (string, error) {
return wf(ctx, v)
},
workflow.RegisterOptions{Name: "wf"})
w.RegisterActivityWithOptions(
func(runId int32) (string, error) {
return act(v, runId)
},
activity.RegisterOptions{Name: "act"})
s.NoError(w.Start())
workers = append(workers, w)
}
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// Workflow should finish, otherwise it may mean we dropped some task without rescheduling them in the new build ID
var out string
s.NoError(run.Get(ctx, &out))
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), versions[9], true, versions[9], "", versions[:9])
activityPerVersion := make(map[string]int)
for _, v := range strings.Split(out, " ") {
activityPerVersion[v]++
}
wh := s.sdkClient.GetWorkflowHistory(ctx, run.GetID(), run.GetRunID(), false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
redirectAppliedToActivityTask := false
activityRetried := false
sawUnorderedEvents := false
var maxBuildId string
var maxStartedTimestamp time.Time
for wh.HasNext() {
he, err := wh.Next()
s.Nil(err)
var taskStartedStamp *commonpb.WorkerVersionStamp
var taskRedirectCounter int64
var buildId string
if activityStarted := he.GetActivityTaskStartedEventAttributes(); activityStarted != nil {
taskStartedStamp = activityStarted.GetWorkerVersion()
buildId = taskStartedStamp.GetBuildId()
if buildId > maxBuildId {
redirectAppliedToActivityTask = true
}
if activityStarted.Attempt > 1 {
activityRetried = true
}
s.True(taskStartedStamp.GetUseVersioning())
taskRedirectCounter = activityStarted.GetBuildIdRedirectCounter()
activityPerVersion[buildId]--
} else if wfStarted := he.GetWorkflowTaskStartedEventAttributes(); wfStarted != nil {
taskStartedStamp = wfStarted.GetWorkerVersion()
if taskStartedStamp != nil {
// taskStartedStamp is nil for sticky queues
s.True(taskStartedStamp.GetUseVersioning())
buildId = taskStartedStamp.GetBuildId()
taskRedirectCounter = wfStarted.GetBuildIdRedirectCounter()
}
}
if he.EventTime.AsTime().Before(maxStartedTimestamp) {
sawUnorderedEvents = true
} else {
maxStartedTimestamp = he.EventTime.AsTime()
}
if buildId > maxBuildId {
maxBuildId = buildId
}
if taskStartedStamp != nil {
// the last char of version is the index in the versions array which is the expected redirect counter for
// a task started event
expectedRedirectCounter, err := strconv.Atoi(buildId[len(buildId)-1:])
s.NoError(err)
s.Equal(expectedRedirectCounter, int(taskRedirectCounter))
}
}
for v, c := range activityPerVersion {
s.Equal(0, c, "activity count mismatch for build ID "+v)
}
// Following validations are more to make sure the test stays correct, rather than testing server's functionality
s.True(activityRetried, "no activity retried")
s.True(sawUnorderedEvents)
s.True(redirectAppliedToActivityTask, "no redirect rule applied to an activity task, is this test broken?")
for _, w := range workers {
w.Stop()
}
}
func (s *VersioningIntegSuite) TestDispatchActivityCompatible() {
s.RunTestWithMatchingBehavior(s.dispatchActivityCompatible)
}
func (s *VersioningIntegSuite) dispatchActivityCompatible() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v11")
started := make(chan struct{}, 2)
act1 := func() (string, error) { return "v1", nil }
act11 := func() (string, error) { return "v1.1", nil }
wf1 := func(ctx workflow.Context) (string, error) {
started <- struct{}{}
// wait for signal
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
// run activity
fut11 := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
ScheduleToCloseTimeout: time.Minute,
DisableEagerExecution: true,
VersioningIntent: temporal.VersioningIntentCompatible,
}), "act")
var val11 string
s.NoError(fut11.Get(ctx, &val11))
return val11, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
w1.RegisterActivityWithOptions(act1, activity.RegisterOptions{Name: "act"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started)
// now register v1.1 as compatible
s.addCompatibleBuildId(ctx, tq, v11, v1, false)
s.waitForVersionSetPropagation(ctx, tq, v11)
// start worker for v1.1
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w11.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
w11.RegisterActivityWithOptions(act11, activity.RegisterOptions{Name: "act"})
s.NoError(w11.Start())
defer w11.Stop()
// wait for w1 long polls to all time out
time.Sleep(longPollTime) //nolint:forbidigo
// unblock the workflow
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), run.GetRunID(), "wait", nil))
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("v1.1", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, false, v11, "", []string{v1})
}
func (s *VersioningIntegSuite) TestDispatchActivityEager() {
s.OverrideDynamicConfig(dynamicconfig.EnableActivityEagerExecution, true)
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.Require().NoError(err)
pollResponse, err := s.sdkClient.WorkflowService().PollWorkflowTaskQueue(ctx, &workflowservice.PollWorkflowTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: tq},
Identity: "test",
WorkerVersionCapabilities: &commonpb.WorkerVersionCapabilities{
BuildId: v1,
},
})
s.Require().NoError(err)
startToCloseTimeout := time.Minute
completionResponse, err := s.sdkClient.WorkflowService().RespondWorkflowTaskCompleted(ctx, &workflowservice.RespondWorkflowTaskCompletedRequest{
Identity: "test",
WorkerVersionStamp: &commonpb.WorkerVersionStamp{
BuildId: v1,
UseVersioning: true,
},
TaskToken: pollResponse.TaskToken,
Commands: []*commandpb.Command{
{
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{
ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
ActivityId: "compatible",
TaskQueue: &taskqueuepb.TaskQueue{
Name: tq,
},
StartToCloseTimeout: durationpb.New(startToCloseTimeout),
ActivityType: &commonpb.ActivityType{
Name: "ignore",
},
RequestEagerExecution: true,
UseWorkflowBuildId: true,
},
},
},
{
CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK,
Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{
ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{
ActivityId: "latest",
TaskQueue: &taskqueuepb.TaskQueue{
Name: tq,
},
StartToCloseTimeout: durationpb.New(startToCloseTimeout),
ActivityType: &commonpb.ActivityType{
Name: "ignore",
},
RequestEagerExecution: true,
UseWorkflowBuildId: false,
},
},
},
},
})
s.Require().NoError(err)
s.Require().Equal(1, len(completionResponse.ActivityTasks))
s.Require().Equal("compatible", completionResponse.ActivityTasks[0].ActivityId)
}
func (s *VersioningIntegSuite) TestDispatchActivityCrossTQFails() {
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1)
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1)
tq := testcore.RandomizeStr(s.T().Name())
crosstq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
act := func() (string, error) { return "v1", nil }
wf := func(ctx workflow.Context) (string, error) {
fut := workflow.ExecuteActivity(workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 1 * time.Second,
TaskQueue: crosstq,
VersioningIntent: temporal.VersioningIntentCompatible,
}), "act")
var val string
s.NoError(fut.Get(ctx, &val))
return val, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.addNewDefaultBuildId(ctx, tq, v1)
s.addNewDefaultBuildId(ctx, crosstq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, crosstq, v1)
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
w1cross := worker.New(s.sdkClient, crosstq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1cross.RegisterActivityWithOptions(act, activity.RegisterOptions{Name: "act"})
s.NoError(w1cross.Start())
defer w1cross.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// workflow should be terminated by invalid argument
var out string
s.Error(run.Get(ctx, &out))
}
func (s *VersioningIntegSuite) TestDispatchChildWorkflowOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchChildWorkflow(false, false) })
}
func (s *VersioningIntegSuite) TestDispatchChildWorkflow() {
s.RunTestWithMatchingBehavior(func() { s.dispatchChildWorkflow(true, false) })
}
func (s *VersioningIntegSuite) TestDispatchChildWorkflowCrossTq() {
s.RunTestWithMatchingBehavior(func() { s.dispatchChildWorkflow(true, true) })
}
func (s *VersioningIntegSuite) dispatchChildWorkflow(newVersioning bool, crossTq bool) {
// This also implicitly tests that a workflow stays on a compatible version set if a new
// incompatible set is registered, because wf2 just panics. It further tests that
// stickiness on v1 is not broken by registering v2, because the channel send will panic on
// replay after we close the channel.
tq := testcore.RandomizeStr(s.T().Name())
childxTq := testcore.RandomizeStr(s.T().Name() + "child")
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
inheritedBuildId := ""
if newVersioning {
inheritedBuildId = v1
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
started := make(chan struct{}, 1)
validateChildBuild := func(cctx workflow.Context, expectedBuildId, expectedInheritedBuildId string) {
exec := workflow.GetInfo(cctx).WorkflowExecution
expectedStampBuildId := ""
if !newVersioning && expectedBuildId == v1 {
expectedStampBuildId = expectedBuildId
}
s.validateWorkflowBuildIds(
ctx,
exec.ID,
exec.RunID,
expectedBuildId,
newVersioning,
expectedStampBuildId,
expectedInheritedBuildId,
nil,
)
if newVersioning {
s.validateWorkflowEventsVersionStamps(ctx, exec.ID, exec.RunID, []string{expectedBuildId}, expectedInheritedBuildId)
}
}
child1 := func(cctx workflow.Context) (string, error) {
validateChildBuild(cctx, v1, inheritedBuildId)
return "v1", nil
}
child2 := func(cctx workflow.Context) (string, error) {
validateChildBuild(cctx, v2, "")
return "v2", nil
}
child1xTq := func(cctx workflow.Context) (string, error) {
validateChildBuild(cctx, v1, inheritedBuildId)
return "v1xTq", nil
}
child2xTq := func(cctx workflow.Context) (string, error) {
validateChildBuild(cctx, v2, "")
return "v2xTq", nil
}
wf1 := func(ctx workflow.Context) (string, error) {
started <- struct{}{}
// wait for signal
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
childTq := tq
if crossTq {
childTq = childxTq
}
// run two child workflows
fut1 := workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
TaskQueue: childTq,
VersioningIntent: temporal.VersioningIntentCompatible,
}), "child")
fut2 := workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
TaskQueue: childTq,
VersioningIntent: temporal.VersioningIntentDefault, // this one should go to default
}), "child")
var val1, val2 string
s.NoError(fut1.Get(ctx, &val1))
s.NoError(fut2.Get(ctx, &val2))
return val1 + val2, nil
}
wf2 := func(ctx workflow.Context) (string, error) {
panic("workflow should not run on v2")
}
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
rule = s.addAssignmentRule(ctx, childxTq, v1)
s.waitForAssignmentRulePropagation(ctx, childxTq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
w1.RegisterWorkflowWithOptions(child1, workflow.RegisterOptions{Name: "child"})
s.NoError(w1.Start())
defer w1.Stop()
w1xTq := worker.New(s.sdkClient, childxTq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1xTq.RegisterWorkflowWithOptions(child1xTq, workflow.RegisterOptions{Name: "child"})
s.NoError(w1xTq.Start())
defer w1xTq.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started)
close(started) // force panic if replayed
// now register v2 as default
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
rule = s.addAssignmentRule(ctx, childxTq, v2)
s.waitForAssignmentRulePropagation(ctx, childxTq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v2)
s.waitForVersionSetPropagation(ctx, tq, v2)
}
// start worker for v2
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
w2.RegisterWorkflowWithOptions(child2, workflow.RegisterOptions{Name: "child"})
s.NoError(w2.Start())
defer w2.Stop()
w2xTq := worker.New(s.sdkClient, childxTq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2xTq.RegisterWorkflowWithOptions(child2xTq, workflow.RegisterOptions{Name: "child"})
s.NoError(w2xTq.Start())
defer w2xTq.Stop()
// unblock the workflow
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), run.GetRunID(), "wait", nil))
var out string
s.NoError(run.Get(ctx, &out))
if crossTq {
s.Equal("v1xTqv2xTq", out)
} else {
s.Equal("v1v2", out)
}
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, newVersioning, v1, "", nil)
}
func (s *VersioningIntegSuite) TestDispatchChildWorkflowUpgradeOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchChildWorkflowUpgrade(false) })
}
func (s *VersioningIntegSuite) TestDispatchChildWorkflowUpgrade() {
s.RunTestWithMatchingBehavior(func() { s.dispatchChildWorkflowUpgrade(true) })
}
func (s *VersioningIntegSuite) dispatchChildWorkflowUpgrade(newVersioning bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v11")
inheritedBuildId := ""
if newVersioning {
inheritedBuildId = v11
}
started := make(chan struct{}, 2)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
child1 := func(cctx workflow.Context) (string, error) {
exec := workflow.GetInfo(cctx).WorkflowExecution
expectedStampBuildId := v1
if newVersioning {
expectedStampBuildId = ""
}
s.validateWorkflowBuildIds(ctx, exec.ID, exec.RunID, v1, newVersioning, expectedStampBuildId, inheritedBuildId, nil)
return "v1", nil
}
child11 := func(cctx workflow.Context) (string, error) {
exec := workflow.GetInfo(cctx).WorkflowExecution
expectedStampBuildId := v11
if newVersioning {
expectedStampBuildId = ""
}
s.validateWorkflowBuildIds(ctx, exec.ID, exec.RunID, v11, newVersioning, expectedStampBuildId, inheritedBuildId, nil)
return "v1.1", nil
}
wf1 := func(ctx workflow.Context) (string, error) {
started <- struct{}{}
// wait for signal
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
// run child
fut11 := workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{}), "child")
var val11 string
s.NoError(fut11.Get(ctx, &val11))
return val11, nil
}
wf11 := func(ctx workflow.Context) (string, error) {
started <- struct{}{}
// wait for signal
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
// run child
fut11 := workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{}), "child")
var val11 string
s.NoError(fut11.Get(ctx, &val11))
return val11, nil
}
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
w1.RegisterWorkflowWithOptions(child1, workflow.RegisterOptions{Name: "child"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started)
// now register v1.1 as compatible
if newVersioning {
rule := s.addRedirectRule(ctx, tq, v1, v11)
s.waitForRedirectRulePropagation(ctx, tq, rule)
} else {
s.addCompatibleBuildId(ctx, tq, v11, v1, false)
s.waitForVersionSetPropagation(ctx, tq, v11)
}
// start worker for v1.1
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w11.RegisterWorkflowWithOptions(wf11, workflow.RegisterOptions{Name: "wf"})
w11.RegisterWorkflowWithOptions(child11, workflow.RegisterOptions{Name: "child"})
s.NoError(w11.Start())
defer w11.Stop()
// wait for w1 long polls to all time out
time.Sleep(longPollTime) //nolint:forbidigo
// unblock the workflow
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), run.GetRunID(), "wait", nil))
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("v1.1", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, newVersioning, v11, "", []string{v1})
}
func (s *VersioningIntegSuite) TestDispatchChildWorkflowCrossTQFails() {
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1)
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1)
tq := testcore.RandomizeStr(s.T().Name())
crosstq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
child := func(ctx workflow.Context) (string, error) { return "v1", nil }
wf := func(ctx workflow.Context) (string, error) {
fut := workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
TaskQueue: crosstq,
VersioningIntent: temporal.VersioningIntentCompatible,
}), "child")
var val string
s.NoError(fut.Get(ctx, &val))
return val, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.addNewDefaultBuildId(ctx, tq, v1)
s.addNewDefaultBuildId(ctx, crosstq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, crosstq, v1)
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
w1cross := worker.New(s.sdkClient, crosstq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1cross.RegisterWorkflowWithOptions(child, workflow.RegisterOptions{Name: "child"})
s.NoError(w1cross.Start())
defer w1cross.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// workflow should be terminated by invalid argument
var out string
s.Error(run.Get(ctx, &out))
}
func (s *VersioningIntegSuite) TestDispatchQueryOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchQuery(false) })
}
func (s *VersioningIntegSuite) TestDispatchQuery() {
s.RunTestWithMatchingBehavior(func() { s.dispatchQuery(true) })
}
func (s *VersioningIntegSuite) dispatchQuery(newVersioning bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v11")
v2 := s.prefixed("v2")
started := make(chan struct{}, 10)
wf1 := func(ctx workflow.Context) error {
if err := workflow.SetQueryHandler(ctx, "query", func() (string, error) { return "v1", nil }); err != nil {
return err
}
started <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
return nil
}
wf11 := func(ctx workflow.Context) error {
if err := workflow.SetQueryHandler(ctx, "query", func() (string, error) { return "v1.1", nil }); err != nil {
return err
}
started <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
return nil
}
wf2 := func(ctx workflow.Context) error {
if err := workflow.SetQueryHandler(ctx, "query", func() (string, error) { return "v2", nil }); err != nil {
return err
}
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started)
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
rrule := s.addRedirectRule(ctx, tq, v1, v11)
s.waitForRedirectRulePropagation(ctx, tq, rrule)
} else {
// now register v1.1 as compatible
// now register v11 as newer compatible with v1 AND v2 as a new default
s.addCompatibleBuildId(ctx, tq, v11, v1, false)
s.addNewDefaultBuildId(ctx, tq, v2)
s.waitForVersionSetPropagation(ctx, tq, v2)
}
// add another 100ms to make sure it got to sticky queues also
time.Sleep(100 * time.Millisecond) //nolint:forbidigo
// start worker for v1.1 and v2
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w11.RegisterWorkflowWithOptions(wf11, workflow.RegisterOptions{Name: "wf"})
s.NoError(w11.Start())
defer w11.Stop()
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2.Start())
defer w2.Stop()
if !newVersioning {
// wait for w1 long polls to all time out
time.Sleep(longPollTime) //nolint:forbidigo
}
// query
val, err := s.sdkClient.QueryWorkflow(ctx, run.GetID(), run.GetRunID(), "query")
s.NoError(err)
var out string
s.NoError(val.Get(&out))
s.Equal("v1.1", out)
// let the workflow complete
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), run.GetRunID(), "wait", nil))
// wait for completion
s.NoError(run.Get(ctx, nil))
// query on closed workflow
val, err = s.sdkClient.QueryWorkflow(ctx, run.GetID(), run.GetRunID(), "query")
s.NoError(err)
s.NoError(val.Get(&out))
s.Equal("v1.1", out)
// start another wf on v2. should complete immediately.
run2, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// wait for completion
s.NoError(run2.Get(ctx, nil))
// query on closed workflow
val, err = s.sdkClient.QueryWorkflow(ctx, run2.GetID(), run2.GetRunID(), "query")
s.NoError(err)
s.NoError(val.Get(&out))
s.Equal("v2", out)
}
func (s *VersioningIntegSuite) TestDispatchContinueAsNewOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchContinueAsNew(false, false) })
}
func (s *VersioningIntegSuite) TestDispatchContinueAsNew() {
s.RunTestWithMatchingBehavior(func() { s.dispatchContinueAsNew(true, false) })
}
func (s *VersioningIntegSuite) TestDispatchContinueAsNewCrossTq() {
s.RunTestWithMatchingBehavior(func() { s.dispatchContinueAsNew(true, true) })
}
func (s *VersioningIntegSuite) dispatchContinueAsNew(newVersioning bool, crossTq bool) {
tq := testcore.RandomizeStr(s.T().Name())
canxTq := testcore.RandomizeStr(s.T().Name() + "CaN")
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
inheritedBuildId := ""
if newVersioning {
inheritedBuildId = v1
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
started1 := make(chan struct{}, 10)
wf1 := func(wctx workflow.Context, attempt int) (string, error) {
switch attempt {
case 0:
s.Equal(tq, workflow.GetInfo(wctx).TaskQueueName)
newCtx := workflow.WithWorkflowVersioningIntent(wctx, temporal.VersioningIntentCompatible)
if crossTq {
newCtx = workflow.WithWorkflowTaskQueue(newCtx, canxTq)
}
started1 <- struct{}{}
return "", workflow.NewContinueAsNewError(newCtx, "wf", attempt+1)
case 1:
exec := workflow.GetInfo(wctx).WorkflowExecution
expectedStampBuildId := v1
if newVersioning {
expectedStampBuildId = ""
}
s.validateWorkflowBuildIds(ctx, exec.ID, exec.RunID, v1, newVersioning, expectedStampBuildId, inheritedBuildId, nil)
if newVersioning {
s.validateWorkflowEventsVersionStamps(ctx, exec.ID, exec.RunID, []string{v1}, inheritedBuildId)
}
workflow.GetSignalChannel(wctx, "wait").Receive(wctx, nil)
if crossTq {
s.Equal(canxTq, workflow.GetInfo(wctx).TaskQueueName)
} else {
s.Equal(tq, workflow.GetInfo(wctx).TaskQueueName)
}
newCtx := workflow.WithWorkflowVersioningIntent(wctx, temporal.VersioningIntentDefault) // this one should go to default
if crossTq {
newCtx = workflow.WithWorkflowTaskQueue(newCtx, canxTq)
}
started1 <- struct{}{}
return "", workflow.NewContinueAsNewError(newCtx, "wf", attempt+1)
case 2:
// return "done!", nil
}
panic("oops")
}
wf2 := func(wctx workflow.Context, attempt int) (string, error) {
if attempt == 2 {
exec := workflow.GetInfo(wctx).WorkflowExecution
s.validateWorkflowBuildIds(ctx, exec.ID, exec.RunID, v2, newVersioning, "", "", nil)
if crossTq {
s.Equal(canxTq, workflow.GetInfo(wctx).TaskQueueName)
} else {
s.Equal(tq, workflow.GetInfo(wctx).TaskQueueName)
}
return "done!", nil
}
panic("oops")
}
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
rule = s.addAssignmentRule(ctx, canxTq, v1)
s.waitForAssignmentRulePropagation(ctx, canxTq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
w1xTq := worker.New(s.sdkClient, canxTq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1xTq.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1xTq.Start())
defer w1xTq.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started1)
// now make v2 as a new default
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
rule = s.addAssignmentRule(ctx, canxTq, v2)
s.waitForAssignmentRulePropagation(ctx, canxTq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v2)
s.waitForVersionSetPropagation(ctx, tq, v2)
}
// add another 100ms to make sure it got to sticky queues also
time.Sleep(100 * time.Millisecond) //nolint:forbidigo
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, newVersioning, v1, "", nil)
// start workers for v2
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2.Start())
defer w2.Stop()
w2xTq := worker.New(s.sdkClient, canxTq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2xTq.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2xTq.Start())
defer w2xTq.Stop()
if !newVersioning {
// wait for w1 long polls to all time out
time.Sleep(longPollTime) //nolint:forbidigo
}
// unblock the workflow. it should get kicked off the sticky queue and replay on v1
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
// wait for it to start on v1
s.WaitForChannel(ctx, started1)
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), "", v2, newVersioning, v2, "", nil)
if newVersioning {
s.validateWorkflowEventsVersionStamps(ctx, run.GetID(), "", []string{v2}, "")
}
}
func (s *VersioningIntegSuite) TestDispatchContinueAsNewUpgradeOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchContinueAsNewUpgrade(false) })
}
func (s *VersioningIntegSuite) TestDispatchContinueAsNewUpgrade() {
s.RunTestWithMatchingBehavior(func() { s.dispatchContinueAsNewUpgrade(true) })
}
func (s *VersioningIntegSuite) dispatchContinueAsNewUpgrade(newVersioning bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v11")
v2 := s.prefixed("v2")
started1 := make(chan struct{}, 10)
started11 := make(chan struct{}, 20)
wf1 := func(ctx workflow.Context, attempt int) (string, error) {
started1 <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
switch attempt {
case 0:
// return "", workflow.NewContinueAsNewError(ctx, "wf", attempt+1)
case 1:
// newCtx := workflow.WithWorkflowVersioningIntent(ctx, temporal.VersioningIntentDefault) // this one should go to default
// return "", workflow.NewContinueAsNewError(newCtx, "wf", attempt+1)
case 2:
// return "done!", nil
}
panic("oops")
}
wf11 := func(ctx workflow.Context, attempt int) (string, error) {
started11 <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
switch attempt {
case 0:
return "", workflow.NewContinueAsNewError(ctx, "wf", attempt+1)
case 1:
newCtx := workflow.WithWorkflowVersioningIntent(ctx, temporal.VersioningIntentDefault) // this one should go to default
return "", workflow.NewContinueAsNewError(newCtx, "wf", attempt+1)
case 2:
// return "done!", nil
}
panic("oops")
}
wf2 := func(ctx workflow.Context, attempt int) (string, error) {
switch attempt {
case 2:
return "done!", nil
}
panic("oops")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started1)
// now register v11 as newer compatible with v1 AND v2 as a new default
if newVersioning {
rule := s.addRedirectRule(ctx, tq, v1, v11)
s.waitForRedirectRulePropagation(ctx, tq, rule)
rule2 := s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule2)
} else {
s.addCompatibleBuildId(ctx, tq, v11, v1, false)
s.addNewDefaultBuildId(ctx, tq, v2)
s.waitForVersionSetPropagation(ctx, tq, v2)
}
// add another 100ms to make sure it got to sticky queues also
time.Sleep(100 * time.Millisecond) //nolint:forbidigo
// start workers for v11 and v2
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w11.RegisterWorkflowWithOptions(wf11, workflow.RegisterOptions{Name: "wf"})
s.NoError(w11.Start())
defer w11.Stop()
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2.Start())
defer w2.Stop()
if !newVersioning {
// wait for w1 long polls to all time out
time.Sleep(longPollTime) //nolint:forbidigo
}
// unblock the workflow. it should get kicked off the sticky queue and replay on v11
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
s.WaitForChannel(ctx, started11)
// then continue-as-new onto v11
s.WaitForChannel(ctx, started11)
// initial run
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, newVersioning, v11, "", []string{v1})
inheritedBuildId := ""
if newVersioning {
inheritedBuildId = v11
}
// first CaN
s.validateWorkflowBuildIds(ctx, run.GetID(), "", v11, newVersioning, v11, inheritedBuildId, nil)
// unblock the second run. it should continue on v11 then continue-as-new onto v2, then
// complete.
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
// second CaN
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), "", v2, newVersioning, v2, "", nil)
}
func (s *VersioningIntegSuite) TestDispatchRetryOld() {
s.RunTestWithMatchingBehavior(s.dispatchRetryOld)
}
func (s *VersioningIntegSuite) dispatchRetryOld() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v11")
v2 := s.prefixed("v2")
started1 := make(chan struct{}, 10)
started11 := make(chan struct{}, 30)
wf1 := func(ctx workflow.Context) (string, error) {
started1 <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
switch workflow.GetInfo(ctx).Attempt {
case 1:
// return "", errors.New("try again")
case 2:
// return "", errors.New("try again")
case 3:
// return "done!", nil
}
panic("oops")
}
wf11 := func(ctx workflow.Context) (string, error) {
started11 <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
switch workflow.GetInfo(ctx).Attempt {
case 1:
return "", errors.New("try again") //nolint:err113
case 2:
return "", errors.New("try again") //nolint:err113
case 3:
return "done!", nil
}
panic("oops")
}
wf2 := func(ctx workflow.Context) (string, error) {
panic("oops")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
TaskQueue: tq,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: 1000 * time.Millisecond,
},
}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started1)
// now register v11 as newer compatible with v1 AND v2 as a new default
s.addCompatibleBuildId(ctx, tq, v11, v1, false)
s.addNewDefaultBuildId(ctx, tq, v2)
s.waitForVersionSetPropagation(ctx, tq, v2)
// add another 100ms to make sure it got to sticky queues also
time.Sleep(100 * time.Millisecond) //nolint:forbidigo
// start workers for v11 and v2
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w11.RegisterWorkflowWithOptions(wf11, workflow.RegisterOptions{Name: "wf"})
s.NoError(w11.Start())
defer w11.Stop()
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2.Start())
defer w2.Stop()
// wait for w1 long polls to all time out
time.Sleep(longPollTime) //nolint:forbidigo
// unblock the workflow. it should replay on v11 and then retry (on v11).
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
s.WaitForChannel(ctx, started11) // replay
s.WaitForChannel(ctx, started11) // attempt 2
// now it's blocked in attempt 2. unblock it.
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
// wait for attempt 3. unblock that and it should return.
s.WaitForChannel(ctx, started11) // attempt 3
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v11, false, v11, "", nil)
}
func (s *VersioningIntegSuite) TestDispatchRetry() {
s.RunTestWithMatchingBehavior(s.dispatchRetry)
}
func (s *VersioningIntegSuite) dispatchRetry() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
started1 := make(chan struct{}, 10)
started2 := make(chan struct{}, 10)
wf1 := func(ctx workflow.Context) (string, error) {
started1 <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
if workflow.GetInfo(ctx).Attempt == 1 {
return "", errors.New("try again") //nolint:err113
}
panic("oops")
}
wf2 := func(ctx workflow.Context) (string, error) {
started2 <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
switch workflow.GetInfo(ctx).Attempt {
case 1:
panic("oops")
case 2:
return "", errors.New("try again") //nolint:err113
case 3:
return "done!", nil
}
panic("oops")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
TaskQueue: tq,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: 1000 * time.Millisecond,
},
}, "wf")
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started1)
// now register v2 as a new default
rule = s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
// add another 100ms to make sure it got to sticky queues also
time.Sleep(100 * time.Millisecond) //nolint:forbidigo
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2.Start())
defer w2.Stop()
// unblock the workflow on v1
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
s.WaitForChannel(ctx, started2) // attempt 2
// now it's blocked in attempt 2. unblock it.
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
// wait for attempt 3. unblock that and it should return.
s.WaitForChannel(ctx, started2) // attempt 3
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v2, true, v2, "", nil)
}
func (s *VersioningIntegSuite) TestDispatchCronOld() {
s.RunTestWithMatchingBehavior(func() { s.dispatchCron(false) })
}
func (s *VersioningIntegSuite) TestDispatchCron() {
s.RunTestWithMatchingBehavior(func() { s.dispatchCron(true) })
}
func (s *VersioningIntegSuite) dispatchCron(newVersioning bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v11")
v2 := s.prefixed("v2")
var runIds1 []string
var runIds2 []string
var runs1 atomic.Int32
var runs11 atomic.Int32
var runs2 atomic.Int32
wf1 := func(ctx workflow.Context) (string, error) {
runs1.Add(1)
runIds1 = append(runIds1, workflow.GetInfo(ctx).WorkflowExecution.RunID)
return "ok", nil
}
wf11 := func(ctx workflow.Context) (string, error) {
runs11.Add(1)
return "ok", nil
}
wf2 := func(ctx workflow.Context) (string, error) {
runs2.Add(1)
runIds2 = append(runIds2, workflow.GetInfo(ctx).WorkflowExecution.RunID)
return "ok", nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
} else {
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflowWithOptions(wf1, workflow.RegisterOptions{Name: "wf"})
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
TaskQueue: tq,
CronSchedule: "@every 1s",
WorkflowExecutionTimeout: 7 * time.Second,
}, "wf")
s.NoError(err)
// give it >=3 runs on v1
s.Eventually(
func() bool {
return runs1.Load() >= int32(3)
},
6*time.Second,
100*time.Millisecond,
)
if newVersioning {
rule := s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
} else {
// now register v11 as newer compatible with v1 AND v2 as a new default.
// it will run on v2 instead of v11 because cron always starts on default.
s.addCompatibleBuildId(ctx, tq, v11, v1, false)
s.addNewDefaultBuildId(ctx, tq, v2)
s.waitForVersionSetPropagation(ctx, tq, v2)
}
// start workers for v11 and v2
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w11.RegisterWorkflowWithOptions(wf11, workflow.RegisterOptions{Name: "wf"})
s.NoError(w11.Start())
defer w11.Stop()
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w2.RegisterWorkflowWithOptions(wf2, workflow.RegisterOptions{Name: "wf"})
s.NoError(w2.Start())
// give it >=3 runs on v2
s.Eventually(
func() bool {
return runs2.Load() >= int32(3)
},
3500*time.Millisecond,
100*time.Millisecond,
)
w2.Stop() // stop w2, because appending to runIds2 while reading from it to validate causes a race
s.Zero(runs11.Load())
for _, runid := range runIds1 {
s.validateWorkflowBuildIds(ctx, run.GetID(), runid, v1, newVersioning, v1, "", nil)
}
for _, runid := range runIds2 {
s.validateWorkflowBuildIds(ctx, run.GetID(), runid, v2, newVersioning, v2, "", nil)
}
}
func (s *VersioningIntegSuite) TestResetWorkflowAssignsToCorrectBuildId() {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
act1 := func() (string, error) {
return "act1 done!", nil
}
wf := func(ctx workflow.Context) (string, error) {
var ret string
err := workflow.ExecuteActivity(
workflow.WithActivityOptions(
ctx, workflow.ActivityOptions{
DisableEagerExecution: true,
StartToCloseTimeout: 1 * time.Second,
},
),
act1,
).Get(ctx, &ret)
s.NoError(err)
s.Equal("act1 done!", ret)
return "done!", nil
}
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
w1 := worker.New(
s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
},
)
w1.RegisterWorkflow(wf)
w1.RegisterActivity(act1)
s.NoError(w1.Start())
defer w1.Stop()
w2 := worker.New(
s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
},
)
w2.RegisterWorkflow(wf)
w2.RegisterActivity(act1)
s.NoError(w2.Start())
defer w2.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
s.validateBuildIdAfterReset(ctx, run.GetID(), run.GetRunID(), false)
}
func (s *VersioningIntegSuite) TestResetWorkflowAssignsToCorrectBuildId_CaN_Inherit() {
s.resetWorkflowAssignsToCorrectBuildIdCan(true)
}
func (s *VersioningIntegSuite) TestResetWorkflowAssignsToCorrectBuildId_CaN_NoInherit() {
s.resetWorkflowAssignsToCorrectBuildIdCan(false)
}
func (s *VersioningIntegSuite) resetWorkflowAssignsToCorrectBuildIdCan(inheritBuildId bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
act1 := func() (string, error) {
return "act1 done!", nil
}
wf := func(ctx workflow.Context, attempt int) (string, error) {
switch attempt {
case 1:
intent := temporal.VersioningIntentDefault
if inheritBuildId {
intent = temporal.VersioningIntentCompatible
}
newCtx := workflow.WithWorkflowVersioningIntent(ctx, intent)
return "", workflow.NewContinueAsNewError(newCtx, "wf", 2)
case 2:
if workflow.GetInfo(ctx).Attempt == 1 {
// failing first attempt of the CaN so we test inherit behavior across retry attempts
return "", errors.New("try again") //nolint:err113
}
var ret string
err := workflow.ExecuteActivity(
workflow.WithActivityOptions(
ctx, workflow.ActivityOptions{
DisableEagerExecution: true,
StartToCloseTimeout: 1 * time.Second,
},
),
act1,
).Get(ctx, &ret)
s.NoError(err)
s.Equal("act1 done!", ret)
return "done!", nil
default:
panic("oops")
}
}
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
w1 := worker.New(
s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
},
)
w1.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{Name: "wf"})
w1.RegisterActivity(act1)
s.NoError(w1.Start())
defer w1.Stop()
w2 := worker.New(
s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
},
)
w2.RegisterWorkflowWithOptions(wf, workflow.RegisterOptions{Name: "wf"})
w2.RegisterActivity(act1)
s.NoError(w2.Start())
defer w2.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{
TaskQueue: tq,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: 1000 * time.Millisecond,
},
}, wf, 1)
s.NoError(err)
s.validateBuildIdAfterReset(ctx, run.GetID(), "", inheritBuildId)
}
func (s *VersioningIntegSuite) TestResetWorkflowAssignsToCorrectBuildId_ChildWF_Inherit() {
s.resetWorkflowAssignsToCorrectBuildIdChildWf(true)
}
func (s *VersioningIntegSuite) TestResetWorkflowAssignsToCorrectBuildId_ChildWF_NoInherit() {
s.resetWorkflowAssignsToCorrectBuildIdChildWf(false)
}
func (s *VersioningIntegSuite) resetWorkflowAssignsToCorrectBuildIdChildWf(inheritBuildId bool) {
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
act1 := func() (string, error) {
return "act1 done!", nil
}
var childWfId string
childStarted := make(chan struct{})
child := func(ctx workflow.Context) (string, error) {
if workflow.GetInfo(ctx).Attempt == 1 {
// failing first attempt of so we test inherit behavior across retry attempts
return "", errors.New("try again") //nolint:err113
}
var ret string
err := workflow.ExecuteActivity(
workflow.WithActivityOptions(
ctx, workflow.ActivityOptions{
DisableEagerExecution: true,
StartToCloseTimeout: 1 * time.Second,
},
),
act1,
).Get(ctx, &ret)
s.NoError(err)
s.Equal("act1 done!", ret)
return "done!", nil
}
wf := func(ctx workflow.Context) (string, error) {
intent := temporal.VersioningIntentDefault
if inheritBuildId {
intent = temporal.VersioningIntentCompatible
}
fut := workflow.ExecuteChildWorkflow(workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
VersioningIntent: intent,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: 1000 * time.Millisecond,
},
}), child)
var val string
var childWE workflow.Execution
s.NoError(fut.GetChildWorkflowExecution().Get(ctx, &childWE))
childWfId = childWE.ID
close(childStarted)
s.NoError(fut.Get(ctx, &val))
s.Equal("done!", val)
return "parent done!", nil
}
rule := s.addAssignmentRule(ctx, tq, v1)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
w1 := worker.New(
s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
},
)
w1.RegisterWorkflow(wf)
w1.RegisterWorkflow(child)
w1.RegisterActivity(act1)
s.NoError(w1.Start())
defer w1.Stop()
w2 := worker.New(
s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
MaxConcurrentWorkflowTaskPollers: numPollers,
},
)
w2.RegisterWorkflow(child)
w2.RegisterActivity(act1)
s.NoError(w2.Start())
defer w2.Stop()
_, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
s.WaitForChannel(ctx, childStarted)
s.validateBuildIdAfterReset(ctx, childWfId, "", inheritBuildId)
}
// assumes given run has a single activity task.
func (s *VersioningIntegSuite) validateBuildIdAfterReset(ctx context.Context, wfId, runId string, expectedInherit bool) {
v1 := s.prefixed("v1")
v2 := s.prefixed("v2")
run := s.sdkClient.GetWorkflow(ctx, wfId, runId)
// let the original run finish
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("done!", out)
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), run.GetRunID())
s.NoError(err)
tq := dw.GetWorkflowExecutionInfo().GetTaskQueue()
inheritedBuildId := dw.GetWorkflowExecutionInfo().GetInheritedBuildId()
s.Equal(expectedInherit, inheritedBuildId != "")
s.validateWorkflowBuildIds(ctx, run.GetID(), run.GetRunID(), v1, true, v1, inheritedBuildId, nil)
// update rules with v2 as the default build
rule := s.addAssignmentRule(ctx, tq, v2)
s.waitForAssignmentRulePropagation(ctx, tq, rule)
// now reset the wf to first wf task
wfr, err := s.sdkClient.ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: run.GetID(),
RunId: run.GetRunID(),
},
WorkflowTaskFinishEventId: 3,
})
s.NoError(err)
// if a build ID is inherited, we should keep using that, otherwise should use the latest rules
expectedBuildId := v2
if inheritedBuildId != "" {
expectedBuildId = inheritedBuildId
}
run2 := s.sdkClient.GetWorkflow(ctx, run.GetID(), wfr.GetRunId())
s.NoError(run2.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run2.GetID(), run2.GetRunID(), expectedBuildId, true, expectedBuildId, inheritedBuildId, nil)
s.validateWorkflowEventsVersionStamps(ctx, run2.GetID(), run2.GetRunID(), []string{
expectedBuildId,
expectedBuildId,
// expectedBuildId, skipped because it belongs to a sticky queue
},
inheritedBuildId)
// now reset the original wf to second wf task and make sure it remains in v1
wfr, err = s.sdkClient.ResetWorkflowExecution(ctx, &workflowservice.ResetWorkflowExecutionRequest{
Namespace: s.Namespace().String(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: run.GetID(),
RunId: run.GetRunID(),
},
WorkflowTaskFinishEventId: 9,
})
s.NoError(err)
run3 := s.sdkClient.GetWorkflow(ctx, run.GetID(), wfr.GetRunId())
s.NoError(run3.Get(ctx, &out))
s.Equal("done!", out)
s.validateWorkflowBuildIds(ctx, run3.GetID(), run3.GetRunID(), v1, true, v1, inheritedBuildId, nil)
s.validateWorkflowEventsVersionStamps(ctx, run3.GetID(), run3.GetRunID(), []string{v1, v1, v1}, inheritedBuildId)
}
func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_Versioned_ReachabilityCache() {
tq := testcore.RandomizeStr(s.T().Name())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. Add assignment rule A and start workflow with build id A
s.addAssignmentRule(ctx, tq, "A")
started := make(chan struct{}, 10)
wf := func(ctx workflow.Context) (string, error) {
started <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
if workflow.GetInfo(ctx).Attempt == 1 {
return "", errors.New("try again") //nolint:err113
}
panic("oops")
}
wId := testcore.RandomizeStr("id")
w := worker.New(s.sdkClient, tq, worker.Options{
UseBuildIDForVersioning: true,
BuildID: "A",
Identity: wId,
})
w.RegisterWorkflow(wf)
s.NoError(w.Start())
defer w.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
s.WaitForChannel(ctx, started)
// 2. Wait for visibility to show A as running with BuildId SearchAttribute 'assigned:A'
s.Eventually(func() bool {
queryARunning := fmt.Sprintf("TaskQueue = '%s' AND BuildIds IN ('assigned:A') AND ExecutionStatus = \"Running\"", tq)
resp, err := s.FrontendClient().CountWorkflowExecutions(ctx, &workflowservice.CountWorkflowExecutionsRequest{
Namespace: s.Namespace().String(),
Query: queryARunning,
})
s.Nil(err)
return resp.GetCount() > 0
}, 5*time.Second, 50*time.Millisecond)
// 3. Commit a different build id --> A should now only be reachable via visibility query
s.commitBuildId(ctx, tq, "B", true, s.getVersioningRules(ctx, tq).GetConflictToken(), true)
// 4. Query reachability(A) --> reachable by visibility db, populating reachability open WF cache with A: true
s.getBuildIdReachability(ctx, tq, &taskqueuepb.TaskQueueVersionSelection{BuildIds: []string{"A"}}, map[string]enumspb.BuildIdTaskReachability{
"A": enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, // reachable by visibility (db)
})
// 5. Signal workflow with build id A so that it completes
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
// 6. Query reachability(A) --> eventually shows closed_only by visibility db (after TTL passes and A is closed in visibility)
s.Eventually(func() bool {
return s.checkBuildIdReachability(ctx, tq, &taskqueuepb.TaskQueueVersionSelection{BuildIds: []string{"A"}}, map[string]enumspb.BuildIdTaskReachability{
"A": enumspb.BUILD_ID_TASK_REACHABILITY_CLOSED_WORKFLOWS_ONLY, // closed_only by visibility db (after TTL)
})
}, 5*time.Second, 50*time.Millisecond)
}
func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_Versioned_BasicReachability() {
tq := testcore.RandomizeStr(s.T().Name())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.getBuildIdReachability(ctx, tq, nil, map[string]enumspb.BuildIdTaskReachability{
"": enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, // reachable because unversioned is default
})
s.addAssignmentRule(ctx, tq, "A")
s.getBuildIdReachability(ctx, tq, &taskqueuepb.TaskQueueVersionSelection{BuildIds: []string{"", "A"}}, map[string]enumspb.BuildIdTaskReachability{
"A": enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, // reachable by default assignment rule
"": enumspb.BUILD_ID_TASK_REACHABILITY_UNREACHABLE, // unreachable because no longer default
})
// start workflow and worker with new default assignment rule "A", and wait for it to start
started := make(chan struct{}, 10)
wf := func(ctx workflow.Context) (string, error) {
started <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
if workflow.GetInfo(ctx).Attempt == 1 {
return "", errors.New("try again") //nolint:err113
}
panic("oops")
}
wId := testcore.RandomizeStr("id")
w := worker.New(s.sdkClient, tq, worker.Options{
UseBuildIDForVersioning: true,
BuildID: "A",
Identity: wId,
})
w.RegisterWorkflow(wf)
s.NoError(w.Start())
defer w.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
s.WaitForChannel(ctx, started)
// wait for visibility to show A as running with BuildId SearchAttribute 'assigned:A'
s.Eventually(func() bool {
queryARunning := fmt.Sprintf("TaskQueue = '%s' AND BuildIds IN ('assigned:A') AND ExecutionStatus = \"Running\"", tq)
resp, err := s.FrontendClient().CountWorkflowExecutions(ctx, &workflowservice.CountWorkflowExecutionsRequest{
Namespace: s.Namespace().String(),
Query: queryARunning,
})
s.Nil(err)
return resp.GetCount() > 0
}, 3*time.Second, 50*time.Millisecond)
// commit a different build ID --> A should now only be reachable via visibility query, B reachable as default
s.commitBuildId(ctx, tq, "B", true, s.getVersioningRules(ctx, tq).GetConflictToken(), true)
s.getBuildIdReachability(ctx, tq, nil, map[string]enumspb.BuildIdTaskReachability{
"B": enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, // reachable by default assignment rule
})
s.getBuildIdReachability(ctx, tq, &taskqueuepb.TaskQueueVersionSelection{BuildIds: []string{"A", "B"}}, map[string]enumspb.BuildIdTaskReachability{
"A": enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, // reachable by visibility
"B": enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, // reachable by default assignment rule
})
// unblock the workflow on A so that it closes
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
// Query reachability(A) --> eventually shows closed_only by visibility db (after TTL passes and A is closed in visibility)
s.Eventually(func() bool {
return s.checkBuildIdReachability(ctx, tq, &taskqueuepb.TaskQueueVersionSelection{BuildIds: []string{"A"}}, map[string]enumspb.BuildIdTaskReachability{
"A": enumspb.BUILD_ID_TASK_REACHABILITY_CLOSED_WORKFLOWS_ONLY, // closed_only by visibility db (after TTL)
"B": enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, // reachable by default assignment rule
})
}, 5*time.Second, 50*time.Millisecond)
}
func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_Unversioned() {
tq := testcore.RandomizeStr(s.T().Name())
wf := func(ctx workflow.Context) (string, error) { return "ok", nil }
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
workerN := 3
workerMap := make(map[string]worker.Worker)
for i := 0; i < workerN; i++ {
wId := testcore.RandomizeStr("id")
w := worker.New(s.sdkClient, tq, worker.Options{
UseBuildIDForVersioning: false,
Identity: wId,
})
w.RegisterWorkflow(wf)
s.NoError(w.Start())
defer w.Stop()
workerMap[wId] = w
}
s.Eventually(func() bool {
resp, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
ApiMode: enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED,
Versions: nil, // default version, in this case unversioned queue
TaskQueueTypes: nil, // both types
ReportPollers: true,
ReportTaskReachability: true,
ReportStats: false,
})
s.NoError(err)
s.NotNil(resp)
s.Assert().Equal(1, len(resp.GetVersionsInfo()), "should be 1 because only default/unversioned queue")
versionInfo := resp.GetVersionsInfo()[""]
s.Assert().Equal(enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, versionInfo.GetTaskReachability())
var pollersInfo []*taskqueuepb.PollerInfo
for _, t := range versionInfo.GetTypesInfo() {
pollersInfo = append(pollersInfo, t.GetPollers()...)
}
foundN := 0
for wId := range workerMap {
for _, pi := range pollersInfo {
s.False(pi.GetWorkerVersionCapabilities().GetUseVersioning())
if pi.GetIdentity() == wId {
foundN++
break
}
}
}
return foundN == workerN
}, 3*time.Second, 50*time.Millisecond)
}
func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_ReportFlags() {
tq := testcore.RandomizeStr(s.T().Name())
wf := func(ctx workflow.Context) (string, error) { return "ok", nil }
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
wId := testcore.RandomizeStr("id")
w := worker.New(s.sdkClient, tq, worker.Options{
UseBuildIDForVersioning: false,
Identity: wId,
})
w.RegisterWorkflow(wf)
s.NoError(w.Start())
defer w.Stop()
// wait for pollers to show up, verify both ReportPollers and ReportTaskReachability
s.Eventually(func() bool {
resp, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
ApiMode: enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED,
Versions: nil, // default version, in this case unversioned queue
TaskQueueTypes: nil, // both types
ReportPollers: true,
ReportTaskReachability: true,
})
s.NoError(err)
s.NotNil(resp)
s.Assert().Equal(1, len(resp.GetVersionsInfo()), "should be 1 because only default/unversioned queue")
versionInfo := resp.GetVersionsInfo()[""]
s.Assert().Equal(enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, versionInfo.GetTaskReachability())
var pollersInfo []*taskqueuepb.PollerInfo
for _, t := range versionInfo.GetTypesInfo() {
pollersInfo = append(pollersInfo, t.GetPollers()...)
}
for _, pi := range pollersInfo {
s.False(pi.GetWorkerVersionCapabilities().GetUseVersioning())
if pi.GetIdentity() == wId {
return true
}
}
return false
}, 3*time.Second, 50*time.Millisecond)
// ask for reachability only
resp, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
ApiMode: enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED,
Versions: nil, // default version, in this case unversioned queue
TaskQueueTypes: nil, // both types
ReportTaskReachability: true,
})
s.NoError(err)
s.NotNil(resp)
s.Assert().Equal(1, len(resp.GetVersionsInfo()), "should be 1 because only default/unversioned queue")
versionInfo := resp.GetVersionsInfo()[""]
s.Assert().Equal(enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, versionInfo.GetTaskReachability())
for _, t := range versionInfo.GetTypesInfo() {
s.Zero(len(t.GetPollers()), "poller info should not be reported")
}
// ask for pollers only
resp, err = s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
ApiMode: enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED,
Versions: nil, // default version, in this case unversioned queue
TaskQueueTypes: nil, // both types
ReportPollers: true,
})
s.NoError(err)
s.NotNil(resp)
s.Assert().Equal(1, len(resp.GetVersionsInfo()), "should be 1 because only default/unversioned queue")
versionInfo = resp.GetVersionsInfo()[""]
s.Assert().Equal(enumspb.BUILD_ID_TASK_REACHABILITY_UNSPECIFIED, versionInfo.GetTaskReachability())
for _, t := range versionInfo.GetTypesInfo() {
s.Equal(1, len(t.GetPollers()), "only one poller info should be reported")
}
}
func (s *VersioningIntegSuite) TestDescribeTaskQueueEnhanced_TooManyBuildIds() {
tq := testcore.RandomizeStr(s.T().Name())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
buildIds := []string{"A", "B", "C", "D"}
resp, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
ApiMode: enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED,
Versions: &taskqueuepb.TaskQueueVersionSelection{BuildIds: buildIds},
TaskQueueTypes: nil, // both types
ReportPollers: false,
ReportTaskReachability: true,
})
s.NoError(err)
s.NotNil(resp)
buildIds = []string{"A", "B", "C", "D", "E"}
resp, err = s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
ApiMode: enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED,
Versions: &taskqueuepb.TaskQueueVersionSelection{BuildIds: buildIds},
TaskQueueTypes: nil, // both types
ReportPollers: false,
ReportTaskReachability: true,
})
s.Error(err)
s.Nil(resp)
}
func (s *VersioningIntegSuite) TestDescribeTaskQueueLegacy_VersionSets() {
// force one partition since DescribeTaskQueue only goes to the root
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 1)
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 1)
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v11")
v2 := s.prefixed("v2")
wf := func(ctx workflow.Context) (string, error) { return "ok", nil }
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.addNewDefaultBuildId(ctx, tq, v1)
s.addCompatibleBuildId(ctx, tq, v11, v1, false)
s.addNewDefaultBuildId(ctx, tq, v2)
s.waitForVersionSetPropagation(ctx, tq, v2)
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
Identity: testcore.RandomizeStr("id"),
})
w1.RegisterWorkflow(wf)
s.NoError(w1.Start())
defer w1.Stop()
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
Identity: testcore.RandomizeStr("id"),
})
w11.RegisterWorkflow(wf)
s.NoError(w11.Start())
defer w11.Stop()
w2 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v2,
UseBuildIDForVersioning: true,
Identity: testcore.RandomizeStr("id"),
})
w2.RegisterWorkflow(wf)
s.NoError(w2.Start())
defer w2.Stop()
s.Eventually(func() bool {
resp, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: tq, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
})
s.NoError(err)
havePoller := func(v string) bool {
for _, p := range resp.Pollers {
if p.WorkerVersionCapabilities.UseVersioning && v == p.WorkerVersionCapabilities.BuildId {
return true
}
}
return false
}
// v1 polls get rejected because v11 is newer
return !havePoller(v1) && havePoller(v11) && havePoller(v2)
}, 3*time.Second, 50*time.Millisecond)
}
func (s *VersioningIntegSuite) TestDescribeWorkflowExecution() {
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueReadPartitions, 4)
s.OverrideDynamicConfig(dynamicconfig.MatchingNumTaskqueueWritePartitions, 4)
tq := testcore.RandomizeStr(s.T().Name())
v1 := s.prefixed("v1")
v11 := s.prefixed("v11")
started1 := make(chan struct{}, 10)
started11 := make(chan struct{}, 10)
wf := func(ctx workflow.Context) (string, error) {
started1 <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
started11 <- struct{}{}
workflow.GetSignalChannel(ctx, "wait").Receive(ctx, nil)
return "ok", nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.addNewDefaultBuildId(ctx, tq, v1)
s.waitForVersionSetPropagation(ctx, tq, v1)
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v1,
UseBuildIDForVersioning: true,
})
w1.RegisterWorkflow(wf)
s.NoError(w1.Start())
defer w1.Stop()
run, err := s.sdkClient.ExecuteWorkflow(ctx, sdkclient.StartWorkflowOptions{TaskQueue: tq}, wf)
s.NoError(err)
// wait for it to start on v1
s.WaitForChannel(ctx, started1)
// describe and check build ID
s.Eventually(func() bool {
resp, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), "")
s.NoError(err)
return v1 == resp.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetBuildId()
}, 5*time.Second, 100*time.Millisecond)
// now register v11 as newer compatible with v1
s.addCompatibleBuildId(ctx, tq, v11, v1, false)
s.waitForVersionSetPropagation(ctx, tq, v11)
// add another 100ms to make sure it got to sticky queues also
time.Sleep(100 * time.Millisecond) //nolint:forbidigo
// start worker for v11
w11 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: v11,
UseBuildIDForVersioning: true,
})
w11.RegisterWorkflow(wf)
s.NoError(w11.Start())
defer w11.Stop()
// wait for w1 long polls to all time out
time.Sleep(longPollTime) //nolint:forbidigo
// unblock the workflow. it should get kicked off the sticky queue and replay on v11
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
s.WaitForChannel(ctx, started11)
s.Eventually(func() bool {
resp, err := s.sdkClient.DescribeWorkflowExecution(ctx, run.GetID(), "")
s.NoError(err)
return v11 == resp.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetBuildId()
}, 5*time.Second, 100*time.Millisecond)
// unblock. it should complete
s.NoError(s.sdkClient.SignalWorkflow(ctx, run.GetID(), "", "wait", nil))
var out string
s.NoError(run.Get(ctx, &out))
s.Equal("ok", out)
}
// Add a per test prefix to avoid hitting the namespace limit of mapped task queue per build ID
func (s *VersioningIntegSuite) prefixed(buildId string) string {
return fmt.Sprintf("t%x:%s", 0xffff&farm.Hash32([]byte(s.T().Name())), buildId)
}
// listVersioningRules lists rules and checks that the result is successful, returning the response.
func (s *VersioningIntegSuite) getVersioningRules(
ctx context.Context, tq string) *workflowservice.GetWorkerVersioningRulesResponse {
res, err := s.FrontendClient().GetWorkerVersioningRules(ctx, &workflowservice.GetWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
})
s.NoError(err)
s.NotNil(res)
return res
}
// insertAssignmentRule replaces a new versioning assignment rule to the task queue user data.
// It checks the response and returns the conflict token.
func (s *VersioningIntegSuite) insertAssignmentRule(
ctx context.Context, tq, newBuildId string,
idx int32, conflictToken []byte, expectSuccess bool) []byte {
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: conflictToken,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_InsertAssignmentRule{
InsertAssignmentRule: &workflowservice.UpdateWorkerVersioningRulesRequest_InsertBuildIdAssignmentRule{
RuleIndex: idx,
Rule: &taskqueuepb.BuildIdAssignmentRule{
TargetBuildId: newBuildId,
},
},
},
})
if expectSuccess {
s.NoError(err)
s.NotNil(res)
s.Assert().Equal(newBuildId, res.GetAssignmentRules()[idx].GetRule().GetTargetBuildId())
return res.GetConflictToken()
} else {
s.Error(err)
s.Nil(res)
return nil
}
}
// replaceAssignmentRule replaces a new versioning assignment rule to the task queue user data.
// It checks the response and returns the conflict token.
func (s *VersioningIntegSuite) replaceAssignmentRule(
ctx context.Context, tq, newBuildId string,
idx int32, conflictToken []byte, expectSuccess bool) []byte {
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: conflictToken,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_ReplaceAssignmentRule{
ReplaceAssignmentRule: &workflowservice.UpdateWorkerVersioningRulesRequest_ReplaceBuildIdAssignmentRule{
RuleIndex: idx,
Rule: &taskqueuepb.BuildIdAssignmentRule{
TargetBuildId: newBuildId,
},
},
},
})
if expectSuccess {
s.NoError(err)
s.NotNil(res)
s.Assert().Equal(newBuildId, res.GetAssignmentRules()[idx].GetRule().GetTargetBuildId())
return res.GetConflictToken()
} else {
s.Error(err)
s.Nil(res)
return nil
}
}
// deleteAssignmentRule deletes the versioning assignment rule at a given index.
// It checks the response and returns the conflict token.
func (s *VersioningIntegSuite) deleteAssignmentRule(
ctx context.Context, tq string,
idx int32, conflictToken []byte, expectSuccess bool) []byte {
getResp, err := s.FrontendClient().GetWorkerVersioningRules(ctx, &workflowservice.GetWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
})
s.NoError(err)
s.NotNil(getResp)
var prevRule *taskqueuepb.BuildIdAssignmentRule
if expectSuccess {
prevRule = getResp.GetAssignmentRules()[idx].GetRule()
}
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: conflictToken,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_DeleteAssignmentRule{
DeleteAssignmentRule: &workflowservice.UpdateWorkerVersioningRulesRequest_DeleteBuildIdAssignmentRule{
RuleIndex: idx,
},
},
})
if expectSuccess {
s.NoError(err)
s.NotNil(res)
found := false
for _, r := range res.GetAssignmentRules() {
if r.GetRule() == prevRule {
found = true
break
}
}
s.Assert().False(found)
return res.GetConflictToken()
} else {
s.Error(err)
s.Nil(res)
return nil
}
}
// insertRedirectRule replaces a new versioning redirect rule to the task queue user data.
// It checks the response and returns the conflict token.
func (s *VersioningIntegSuite) insertRedirectRule(
ctx context.Context, tq, sourceBuildId, targetBuildId string,
conflictToken []byte, expectSuccess bool) []byte {
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: conflictToken,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_AddCompatibleRedirectRule{
AddCompatibleRedirectRule: &workflowservice.UpdateWorkerVersioningRulesRequest_AddCompatibleBuildIdRedirectRule{
Rule: &taskqueuepb.CompatibleBuildIdRedirectRule{
SourceBuildId: sourceBuildId,
TargetBuildId: targetBuildId,
},
},
},
})
if expectSuccess {
s.NoError(err)
s.NotNil(res)
found := false
for _, r := range res.GetCompatibleRedirectRules() {
if r.GetRule().GetSourceBuildId() == sourceBuildId && r.GetRule().GetTargetBuildId() == targetBuildId {
found = true
break
}
}
s.Assert().True(found)
return res.GetConflictToken()
} else {
s.Error(err)
s.Nil(res)
return nil
}
}
// replaceRedirectRule replaces a new versioning redirect rule to the task queue user data.
// It checks the response and returns the conflict token.
func (s *VersioningIntegSuite) replaceRedirectRule(
ctx context.Context, tq, sourceBuildId, targetBuildId string,
conflictToken []byte, expectSuccess bool) []byte {
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: conflictToken,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_ReplaceCompatibleRedirectRule{
ReplaceCompatibleRedirectRule: &workflowservice.UpdateWorkerVersioningRulesRequest_ReplaceCompatibleBuildIdRedirectRule{
Rule: &taskqueuepb.CompatibleBuildIdRedirectRule{
SourceBuildId: sourceBuildId,
TargetBuildId: targetBuildId,
},
},
},
})
if expectSuccess {
s.NoError(err)
s.NotNil(res)
found := false
for _, r := range res.GetCompatibleRedirectRules() {
if r.GetRule().GetSourceBuildId() == sourceBuildId && r.GetRule().GetTargetBuildId() == targetBuildId {
found = true
break
}
}
s.Assert().True(found)
return res.GetConflictToken()
} else {
s.Error(err)
s.Nil(res)
return nil
}
}
// deleteRedirectRule deletes the versioning redirect rule at a given index.
// It checks the response and returns the conflict token.
func (s *VersioningIntegSuite) deleteRedirectRule(
ctx context.Context, tq, sourceBuildId string,
conflictToken []byte, expectSuccess bool) []byte {
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: conflictToken,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_DeleteCompatibleRedirectRule{
DeleteCompatibleRedirectRule: &workflowservice.UpdateWorkerVersioningRulesRequest_DeleteCompatibleBuildIdRedirectRule{
SourceBuildId: sourceBuildId,
},
},
})
if expectSuccess {
s.NoError(err)
s.NotNil(res)
found := false
for _, r := range res.GetCompatibleRedirectRules() {
if r.GetRule().GetSourceBuildId() == sourceBuildId {
found = true
break
}
}
s.Assert().False(found)
return res.GetConflictToken()
} else {
s.Error(err)
s.Nil(res)
return nil
}
}
// commitBuildId sends a CommitBuildId request for the given build ID
// It checks the response and returns the conflict token.
func (s *VersioningIntegSuite) commitBuildId(
ctx context.Context, tq, targetBuildId string, force bool,
conflictToken []byte, expectSuccess bool) []byte {
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: conflictToken,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_CommitBuildId_{
CommitBuildId: &workflowservice.UpdateWorkerVersioningRulesRequest_CommitBuildId{
TargetBuildId: targetBuildId,
Force: force,
},
},
})
if expectSuccess {
s.NoError(err)
s.NotNil(res)
// 1. Adds a fully-ramped assignment rule for the target Build ID at the end of the list.
endIdx := len(res.GetAssignmentRules()) - 1
addedRule := res.GetAssignmentRules()[endIdx].GetRule()
s.Assert().Equal(targetBuildId, addedRule.GetTargetBuildId())
s.Assert().Equal(float32(100), addedRule.GetPercentageRamp().GetRampPercentage())
foundOtherAssignmentRuleForTarget := false
foundFullyRampedAssignmentRuleForOtherTarget := false
for i, r := range res.GetAssignmentRules() {
if r.GetRule().GetTargetBuildId() == targetBuildId && i != endIdx {
foundOtherAssignmentRuleForTarget = true
}
if r.GetRule().GetPercentageRamp().GetRampPercentage() == 100 && r.GetRule().GetTargetBuildId() != targetBuildId {
foundFullyRampedAssignmentRuleForOtherTarget = true
}
}
// 2. Removes all previously added assignment rules to the given target Build ID (if any).
s.Assert().False(foundOtherAssignmentRuleForTarget)
// 3. Removes any fully-ramped assignment rule for other Build IDs.
s.Assert().False(foundFullyRampedAssignmentRuleForOtherTarget)
return res.GetConflictToken()
} else {
s.Error(err)
s.Nil(res)
return nil
}
}
func (s *VersioningIntegSuite) registerWorkflowAndPollVersionedTaskQueue(tq, buildID string, useVersioning bool) {
wf := func(ctx workflow.Context) (string, error) {
return "done!", nil
}
w1 := worker.New(s.sdkClient, tq, worker.Options{
BuildID: buildID,
UseBuildIDForVersioning: useVersioning,
MaxConcurrentWorkflowTaskPollers: numPollers,
})
w1.RegisterWorkflow(wf)
s.NoError(w1.Start())
defer w1.Stop()
// wait for it to start polling
time.Sleep(200 * time.Millisecond) //nolint:forbidigo
}
func (s *VersioningIntegSuite) getBuildIdReachability(
ctx context.Context,
taskQueue string,
versions *taskqueuepb.TaskQueueVersionSelection,
expectedReachability map[string]enumspb.BuildIdTaskReachability) {
resp, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
ApiMode: enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED,
Versions: versions,
TaskQueueTypes: nil, // both types
ReportPollers: false,
ReportTaskReachability: true,
})
s.NoError(err)
s.NotNil(resp)
for buildId, vi := range resp.GetVersionsInfo() {
expected, ok := expectedReachability[buildId]
s.Assert().True(ok, "build id %s was not expected", buildId)
s.Assert().Equal(expected, vi.GetTaskReachability(), "build id %s has unexpected reachability", buildId)
}
}
func (s *VersioningIntegSuite) checkBuildIdReachability(
ctx context.Context,
taskQueue string,
versions *taskqueuepb.TaskQueueVersionSelection,
expectedReachability map[string]enumspb.BuildIdTaskReachability) bool {
resp, err := s.FrontendClient().DescribeTaskQueue(ctx, &workflowservice.DescribeTaskQueueRequest{
Namespace: s.Namespace().String(),
TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
ApiMode: enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED,
Versions: versions,
TaskQueueTypes: nil, // both types
ReportPollers: false,
ReportTaskReachability: true,
})
if err != nil {
return false
}
if resp == nil {
return false
}
for buildId, vi := range resp.GetVersionsInfo() {
expected, ok := expectedReachability[buildId]
if !ok {
return false // build id was not expected
}
if expected != vi.GetTaskReachability() {
return false // build id has unexpected reachability
}
}
return true
}
// addNewDefaultBuildId updates build ID info on a task queue with a new build ID in a new default set.
func (s *VersioningIntegSuite) addNewDefaultBuildId(ctx context.Context, tq, newBuildId string) {
res, err := s.FrontendClient().UpdateWorkerBuildIdCompatibility(ctx, &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
Operation: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewBuildIdInNewDefaultSet{
AddNewBuildIdInNewDefaultSet: newBuildId,
},
})
s.NoError(err)
s.NotNil(res)
}
func (s *VersioningIntegSuite) addAssignmentRule(ctx context.Context, tq, buildId string) *taskqueuepb.BuildIdAssignmentRule {
return s.addAssignmentRuleWithRamp(ctx, tq, buildId, 100)
}
func (s *VersioningIntegSuite) addAssignmentRuleWithRamp(ctx context.Context, tq, buildId string, ramp float32) *taskqueuepb.BuildIdAssignmentRule {
rule := &taskqueuepb.BuildIdAssignmentRule{
TargetBuildId: buildId,
Ramp: &taskqueuepb.BuildIdAssignmentRule_PercentageRamp{
PercentageRamp: &taskqueuepb.RampByPercentage{
RampPercentage: ramp,
},
},
}
return s.doAddAssignmentRule(ctx, tq, rule)
}
func (s *VersioningIntegSuite) doAddAssignmentRule(ctx context.Context, tq string, rule *taskqueuepb.BuildIdAssignmentRule) *taskqueuepb.BuildIdAssignmentRule {
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: cT,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_InsertAssignmentRule{
InsertAssignmentRule: &workflowservice.UpdateWorkerVersioningRulesRequest_InsertBuildIdAssignmentRule{
Rule: rule,
},
},
})
s.NoError(err)
s.NotNil(res)
return rule
}
func (s *VersioningIntegSuite) addRedirectRule(ctx context.Context, tq, source string, target string) *taskqueuepb.CompatibleBuildIdRedirectRule {
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
rule := &taskqueuepb.CompatibleBuildIdRedirectRule{
SourceBuildId: source,
TargetBuildId: target,
}
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: cT,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_AddCompatibleRedirectRule{
AddCompatibleRedirectRule: &workflowservice.UpdateWorkerVersioningRulesRequest_AddCompatibleBuildIdRedirectRule{
Rule: rule,
},
},
})
s.NoError(err)
s.NotNil(res)
return rule
}
func (s *VersioningIntegSuite) removeRedirectRule(ctx context.Context, tq, source string) {
cT := s.getVersioningRules(ctx, tq).GetConflictToken()
res, err := s.FrontendClient().UpdateWorkerVersioningRules(ctx, &workflowservice.UpdateWorkerVersioningRulesRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
ConflictToken: cT,
Operation: &workflowservice.UpdateWorkerVersioningRulesRequest_DeleteCompatibleRedirectRule{
DeleteCompatibleRedirectRule: &workflowservice.UpdateWorkerVersioningRulesRequest_DeleteCompatibleBuildIdRedirectRule{
SourceBuildId: source,
},
},
})
s.NoError(err)
s.NotNil(res)
}
// addCompatibleBuildId updates build ID info on a task queue with a new compatible build ID.
func (s *VersioningIntegSuite) addCompatibleBuildId(ctx context.Context, tq, newBuildId, existing string, makeSetDefault bool) {
res, err := s.FrontendClient().UpdateWorkerBuildIdCompatibility(ctx, &workflowservice.UpdateWorkerBuildIdCompatibilityRequest{
Namespace: s.Namespace().String(),
TaskQueue: tq,
Operation: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewCompatibleBuildId{
AddNewCompatibleBuildId: &workflowservice.UpdateWorkerBuildIdCompatibilityRequest_AddNewCompatibleVersion{
NewBuildId: newBuildId,
ExistingCompatibleBuildId: existing,
MakeSetDefault: makeSetDefault,
},
},
})
s.NoError(err)
s.NotNil(res)
}
// waitForVersionSetPropagation waits for all partitions of tq to mention newBuildId in their versioning data (in any position).
func (s *VersioningIntegSuite) waitForVersionSetPropagation(ctx context.Context, taskQueue, newBuildId string) {
s.waitForPropagation(ctx, taskQueue, 0, func(vd *persistencespb.VersioningData) bool {
for _, set := range vd.GetVersionSets() {
for _, id := range set.BuildIds {
if id.Id == newBuildId {
return true
}
}
}
return false
})
}
// waitForAssignmentRulePropagation waits for all partitions of tq to have the given assignment rule in their versioning data
func (s *VersioningIntegSuite) waitForAssignmentRulePropagation(ctx context.Context, taskQueue string, rule *taskqueuepb.BuildIdAssignmentRule) {
s.waitForPropagation(ctx, taskQueue, 0, func(vd *persistencespb.VersioningData) bool {
for _, r := range vd.GetAssignmentRules() {
if r.GetRule().Equal(rule) {
return true
}
}
return false
})
}
// waitForRedirectRulePropagation waits for all partitions of tq to have the given redirect rule in their versioning data
func (s *VersioningIntegSuite) waitForRedirectRulePropagation(ctx context.Context, taskQueue string, rule *taskqueuepb.CompatibleBuildIdRedirectRule) {
s.waitForPropagation(ctx, taskQueue, 0, func(vd *persistencespb.VersioningData) bool {
for _, r := range vd.GetRedirectRules() {
if r.GetRule().Equal(rule) {
return true
}
}
return false
})
}
// waitForRedirectRulePropagationUpToPartition waits for partitions of tq, up to a certain partition ID, to have the
// given redirect rule in their versioning data
func (s *VersioningIntegSuite) waitForRedirectRulePropagationUpToPartition(
ctx context.Context,
taskQueue string,
rule *taskqueuepb.CompatibleBuildIdRedirectRule,
upToPartition int,
) {
s.waitForPropagation(ctx, taskQueue, upToPartition+1, func(vd *persistencespb.VersioningData) bool {
for _, r := range vd.GetRedirectRules() {
if r.GetRule().Equal(rule) {
return true
}
}
return false
})
}
// waitForPropagation waits for all partitions of tq to mention newBuildId in their versioning data (in any position).
// Pass 0 for partitionCount to make it load from MatchingNumTaskqueueReadPartitions config.
func (s *VersioningIntegSuite) waitForPropagation(
ctx context.Context,
taskQueue string,
partitionCount int,
condition func(data *persistencespb.VersioningData) bool,
) {
if partitionCount <= 0 {
v := s.GetTestCluster().Host().DcClient().GetValue(dynamicconfig.MatchingNumTaskqueueReadPartitions.Key())
s.NotEmpty(v, "versioning tests require setting explicit number of partitions")
count, ok := v[0].Value.(int)
s.True(ok, "partition count is not an int")
partitionCount = count
}
type partAndType struct {
part int
tp enumspb.TaskQueueType
}
remaining := make(map[partAndType]struct{})
for i := 0; i < partitionCount; i++ {
remaining[partAndType{i, enumspb.TASK_QUEUE_TYPE_ACTIVITY}] = struct{}{}
remaining[partAndType{i, enumspb.TASK_QUEUE_TYPE_WORKFLOW}] = struct{}{}
}
s.Eventually(func() bool {
for pt := range remaining {
f, err := tqid.NewTaskQueueFamily(s.NamespaceID().String(), taskQueue)
s.NoError(err)
partition := f.TaskQueue(pt.tp).NormalPartition(pt.part)
// Use lower-level GetTaskQueueUserData instead of GetWorkerBuildIdCompatibility
// here so that we can target activity queues.
res, err := s.GetTestCluster().Host().MatchingClient().GetTaskQueueUserData(
ctx,
&matchingservice.GetTaskQueueUserDataRequest{
NamespaceId: s.NamespaceID().String(),
TaskQueue: partition.RpcName(),
TaskQueueType: partition.TaskType(),
})
s.NoError(err)
if condition(res.GetUserData().GetData().GetVersioningData()) {
delete(remaining, pt)
}
}
return len(remaining) == 0
}, 10*time.Second, 100*time.Millisecond)
}
func (s *VersioningIntegSuite) unloadTaskQueue(ctx context.Context, tq string) {
_, err := s.GetTestCluster().MatchingClient().ForceUnloadTaskQueuePartition(ctx, &matchingservice.ForceUnloadTaskQueuePartitionRequest{
NamespaceId: s.NamespaceID().String(),
TaskQueuePartition: &taskqueuespb.TaskQueuePartition{
TaskQueue: tq,
TaskQueueType: enumspb.TASK_QUEUE_TYPE_WORKFLOW,
},
})
s.Require().NoError(err)
}
func (s *VersioningIntegSuite) getStickyQueueName(ctx context.Context, id string) string {
ms, err := s.AdminClient().DescribeMutableState(ctx, &adminservice.DescribeMutableStateRequest{
Namespace: s.Namespace().String(),
Execution: &commonpb.WorkflowExecution{WorkflowId: id},
})
s.NoError(err)
return ms.DatabaseMutableState.ExecutionInfo.StickyTaskQueue
}
func getCurrentDefault(res *workflowservice.GetWorkerBuildIdCompatibilityResponse) string {
if res == nil {
return ""
}
curMajorSet := res.GetMajorVersionSets()[len(res.GetMajorVersionSets())-1]
return curMajorSet.GetBuildIds()[len(curMajorSet.GetBuildIds())-1]
}
// Periodically checks a WF and unblocks when it is assigned to the given build ID
func (s *VersioningIntegSuite) waitForWorkflowBuildId(
ctx context.Context,
wfId string,
runId string,
buildId string,
) {
s.Eventually(
func() bool {
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, wfId, runId)
if err != nil {
return false
}
return dw.GetWorkflowExecutionInfo().GetAssignedBuildId() == buildId
},
10*time.Second,
100*time.Millisecond,
)
}
func (s *VersioningIntegSuite) validateWorkflowBuildIds(
ctx context.Context,
wfId string,
runId string,
expectedBuildId string,
newVersioning bool,
expectedStampBuildId string,
expectedInheritedBuildId string,
extraSearchAttrBuildIds []string,
) {
dw, err := s.sdkClient.DescribeWorkflowExecution(ctx, wfId, runId)
s.NoError(err)
saPayload := dw.GetWorkflowExecutionInfo().GetSearchAttributes().GetIndexedFields()["BuildIds"]
searchAttrAny, err := searchattribute.DecodeValue(saPayload, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST, true)
var searchAttr []string
if searchAttrAny != nil {
searchAttr = searchAttrAny.([]string)
}
s.NoError(err)
if expectedBuildId == "" {
if expectedStampBuildId != "" {
s.NotNil(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetBuildId())
s.False(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetUseVersioning())
s.Equal(2+len(extraSearchAttrBuildIds), len(searchAttr))
s.Equal(worker_versioning.UnversionedSearchAttribute, searchAttr[0])
s.True(strings.HasPrefix(searchAttr[1], worker_versioning.UnversionedSearchAttribute))
} else {
s.Nil(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp())
s.Equal(0, len(searchAttr))
}
} else {
if expectedStampBuildId != "" {
s.True(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetUseVersioning())
s.Equal(expectedStampBuildId, dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp().GetBuildId())
} else {
s.Nil(dw.GetWorkflowExecutionInfo().GetMostRecentWorkerVersionStamp())
}
if newVersioning {
s.Equal(expectedBuildId, dw.GetWorkflowExecutionInfo().GetAssignedBuildId())
s.Equal(2+len(extraSearchAttrBuildIds), len(searchAttr))
s.Equal(worker_versioning.AssignedBuildIdSearchAttribute(expectedBuildId), searchAttr[0])
s.Contains(searchAttr, worker_versioning.VersionedBuildIdSearchAttribute(expectedBuildId))
} else {
s.Equal("", dw.GetWorkflowExecutionInfo().GetAssignedBuildId())
if expectedStampBuildId != "" {
s.Equal(1+len(extraSearchAttrBuildIds), len(searchAttr))
s.Contains(searchAttr, worker_versioning.VersionedBuildIdSearchAttribute(expectedBuildId))
} else {
s.Equal(0, len(searchAttr))
}
}
}
s.Equal(expectedInheritedBuildId, dw.GetWorkflowExecutionInfo().GetInheritedBuildId())
for _, b := range extraSearchAttrBuildIds {
if expectedBuildId == "" {
s.Contains(searchAttr, worker_versioning.UnversionedBuildIdSearchAttribute(b))
} else {
s.Contains(searchAttr, worker_versioning.VersionedBuildIdSearchAttribute(b))
}
}
}
func (s *VersioningIntegSuite) validateWorkflowEventsVersionStamps(
ctx context.Context,
wfId, runId string,
expectedBuildIds []string,
expectedInheritedBuildId string,
) {
wh := s.sdkClient.GetWorkflowHistory(ctx, wfId, runId, false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
counter := 0
checkedInheritedBuildId := false
for wh.HasNext() {
he, err := wh.Next()
s.Nil(err)
if !checkedInheritedBuildId {
// first event
checkedInheritedBuildId = true
s.Equal(expectedInheritedBuildId, he.GetWorkflowExecutionStartedEventAttributes().GetInheritedBuildId())
}
var taskStartedStamp *commonpb.WorkerVersionStamp
if activityStarted := he.GetActivityTaskStartedEventAttributes(); activityStarted != nil {
taskStartedStamp = activityStarted.GetWorkerVersion()
} else if wfStarted := he.GetWorkflowTaskStartedEventAttributes(); wfStarted != nil {
taskStartedStamp = wfStarted.GetWorkerVersion()
}
if taskStartedStamp != nil {
if counter >= len(expectedBuildIds) {
s.Fail("found more task started events than expected")
}
expected := expectedBuildIds[counter]
if expected == "" {
s.False(taskStartedStamp.GetUseVersioning())
} else {
s.True(taskStartedStamp.GetUseVersioning())
s.Equal(expected, taskStartedStamp.GetBuildId())
}
counter++
}
}
if counter != len(expectedBuildIds) {
s.Fail("found less task started events than expected")
}
}