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