Files
Carly de Frondevilleandrodrigozhou b15ff86e1d set status to DRAINING immediately after stop accepting new workflows (#7343)
## What changed?
Set version to `DRAINING` before starting child instead of relying on
child's signal

## Why?
<!-- Tell your future self why have you made these changes -->

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

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

## 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/`? -->

## Is hotfix candidate?
<!-- Is this PR a hotfix candidate or does it require a notification to
be sent to the broader community? (Yes/No) -->
2025-02-20 18:07:18 -08:00

731 lines
28 KiB
Go

// The MIT License
//
// Copyright (c) 2024 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2024 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.
package workerdeployment
import (
"bytes"
"fmt"
"slices"
"github.com/pborman/uuid"
deploymentpb "go.temporal.io/api/deployment/v1"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
sdkclient "go.temporal.io/sdk/client"
sdklog "go.temporal.io/sdk/log"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
deploymentspb "go.temporal.io/server/api/deployment/v1"
"go.temporal.io/server/common/worker_versioning"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
// The actual limit is set in dynamic configs, this is only used in case we cannot read the DC.
defaultMaxVersions = 100
)
type (
// WorkflowRunner holds the local state while running a deployment-series workflow
WorkflowRunner struct {
*deploymentspb.WorkerDeploymentWorkflowArgs
a *Activities
logger sdklog.Logger
metrics sdkclient.MetricsHandler
lock workflow.Mutex
pendingUpdates int
conflictToken []byte
done bool
signalsCompleted bool
unsafeMaxVersion func() int
}
)
func Workflow(ctx workflow.Context, unsafeMaxVersion func() int, args *deploymentspb.WorkerDeploymentWorkflowArgs) error {
workflowRunner := &WorkflowRunner{
WorkerDeploymentWorkflowArgs: args,
a: nil,
logger: sdklog.With(workflow.GetLogger(ctx), "wf-namespace", args.NamespaceName),
metrics: workflow.GetMetricsHandler(ctx).WithTags(map[string]string{"namespace": args.NamespaceName}),
lock: workflow.NewMutex(ctx),
unsafeMaxVersion: unsafeMaxVersion,
}
return workflowRunner.run(ctx)
}
func (d *WorkflowRunner) listenToSignals(ctx workflow.Context) {
forceCANSignalChannel := workflow.GetSignalChannel(ctx, ForceCANSignalName)
syncVersionSummaryChannel := workflow.GetSignalChannel(ctx, SyncVersionSummarySignal)
forceCAN := false
selector := workflow.NewSelector(ctx)
selector.AddReceive(forceCANSignalChannel, func(c workflow.ReceiveChannel, more bool) {
c.Receive(ctx, nil)
forceCAN = true
})
selector.AddReceive(syncVersionSummaryChannel, func(c workflow.ReceiveChannel, more bool) {
var summary *deploymentspb.WorkerDeploymentVersionSummary
c.Receive(ctx, &summary)
if _, ok := d.State.Versions[summary.GetVersion()]; !ok {
d.logger.Error("received summary for a non-existing version, ignoring it", "version", summary.GetVersion())
}
d.State.Versions[summary.GetVersion()] = summary
})
for (!workflow.GetInfo(ctx).GetContinueAsNewSuggested() && !forceCAN) || selector.HasPending() {
selector.Select(ctx)
}
// Done processing signals before CAN
d.signalsCompleted = true
}
func (d *WorkflowRunner) run(ctx workflow.Context) error {
if d.State == nil {
d.State = &deploymentspb.WorkerDeploymentLocalState{}
d.State.CreateTime = timestamppb.New(workflow.Now(ctx))
d.State.RoutingConfig = &deploymentpb.RoutingConfig{CurrentVersion: worker_versioning.UnversionedVersionId}
d.State.ConflictToken, _ = workflow.Now(ctx).MarshalBinary()
// updating the memo since the RoutingConfig is updated
if err := d.updateMemo(ctx); err != nil {
return err
}
}
if d.State.Versions == nil {
d.State.Versions = make(map[string]*deploymentspb.WorkerDeploymentVersionSummary)
}
err := workflow.SetQueryHandler(ctx, QueryDescribeDeployment, func() (*deploymentspb.QueryDescribeWorkerDeploymentResponse, error) {
return &deploymentspb.QueryDescribeWorkerDeploymentResponse{
State: d.State,
}, nil
})
if err != nil {
d.logger.Info("SetQueryHandler failed for WorkerDeployment workflow with error: " + err.Error())
return err
}
if err := workflow.SetUpdateHandlerWithOptions(
ctx,
SetCurrentVersion,
d.handleSetCurrent,
workflow.UpdateHandlerOptions{
Validator: d.validateSetCurrent,
},
); err != nil {
return err
}
if err := workflow.SetUpdateHandlerWithOptions(
ctx,
SetRampingVersion,
d.handleSetRampingVersion,
workflow.UpdateHandlerOptions{
Validator: d.validateSetRampingVersion,
},
); err != nil {
return err
}
if err := workflow.SetUpdateHandlerWithOptions(
ctx,
AddVersionToWorkerDeployment,
d.handleAddVersionToWorkerDeployment,
workflow.UpdateHandlerOptions{
Validator: d.validateAddVersionToWorkerDeployment,
},
); err != nil {
return err
}
if err := workflow.SetUpdateHandlerWithOptions(
ctx,
DeleteVersion,
d.handleDeleteVersion,
workflow.UpdateHandlerOptions{
Validator: d.validateDeleteVersion,
},
); err != nil {
return err
}
if err := workflow.SetUpdateHandlerWithOptions(
ctx,
DeleteDeployment,
d.handleDeleteDeployment,
workflow.UpdateHandlerOptions{
Validator: d.validateDeleteDeployment,
},
); err != nil {
return err
}
// Listen to signals in a different goroutine to make business logic clearer
workflow.Go(ctx, d.listenToSignals)
// Wait until we can continue as new or are cancelled.
err = workflow.Await(ctx, func() bool {
return (d.signalsCompleted && d.pendingUpdates == 0) || d.done
})
if err != nil {
return err
}
if d.done {
return nil
}
// Continue as new when there are no pending updates and history size is greater than requestsBeforeContinueAsNew.
// Note, if update requests come in faster than they
// are handled, there will not be a moment where the workflow has
// nothing pending which means this will run forever.
return workflow.NewContinueAsNewError(ctx, WorkerDeploymentWorkflowType, d.WorkerDeploymentWorkflowArgs)
}
func (d *WorkflowRunner) validateDeleteDeployment() error {
if len(d.State.Versions) > 0 {
return serviceerror.NewFailedPrecondition("deployment has versions, can't be deleted")
}
return nil
}
func (d *WorkflowRunner) handleDeleteDeployment(ctx workflow.Context) error {
if len(d.State.Versions) == 0 {
d.done = true
}
return nil
}
func (d *WorkflowRunner) validateStateBeforeAcceptingRampingUpdate(args *deploymentspb.SetRampingVersionArgs) error {
if args.Version == d.State.RoutingConfig.RampingVersion && args.Percentage == d.State.RoutingConfig.RampingVersionPercentage && args.Identity == d.State.LastModifierIdentity {
d.logger.Info("version already ramping, no change")
return temporal.NewApplicationError("version already ramping, no change", errNoChangeType, d.State.ConflictToken)
}
if args.ConflictToken != nil && !bytes.Equal(args.ConflictToken, d.State.ConflictToken) {
return temporal.NewApplicationError("conflict token mismatch", errConflictTokenMismatchType)
}
if args.Version == d.State.RoutingConfig.CurrentVersion {
d.logger.Info("version can't be set to ramping since it is already current")
return temporal.NewApplicationError("version can't be set to ramping since it is already current", errVersionAlreadyCurrentType)
}
if _, ok := d.State.Versions[args.Version]; !ok && args.Version != "" && args.Version != worker_versioning.UnversionedVersionId {
d.logger.Info("version not found in deployment")
return temporal.NewApplicationError("version not found in deployment", errVersionNotFound)
}
return nil
}
func (d *WorkflowRunner) validateSetRampingVersion(args *deploymentspb.SetRampingVersionArgs) error {
return d.validateStateBeforeAcceptingRampingUpdate(args)
}
//revive:disable-next-line:cognitive-complexity
func (d *WorkflowRunner) handleSetRampingVersion(ctx workflow.Context, args *deploymentspb.SetRampingVersionArgs) (*deploymentspb.SetRampingVersionResponse, error) {
// use lock to enforce only one update at a time
err := d.lock.Lock(ctx)
if err != nil {
d.logger.Error("Could not acquire workflow lock")
return nil, serviceerror.NewDeadlineExceeded("Could not acquire workflow lock")
}
d.pendingUpdates++
defer func() {
d.pendingUpdates--
d.lock.Unlock()
}()
// Validating the state before starting the SetRampingVersion operation. This is required due to the following reason:
// The validator accepts/rejects updates based on the state of the deployment workflow. Theoretically, two concurrent update requests
// might be accepted by the validator since the state of the workflow, at that point in time, is valid for the updates to take place. Since this update handler
// enforces sequential updates, after the first update completes, the local state of the deployment workflow will change. The second update,
// now already accepted by the validator, should now not be allowed to run since the state of the workflow is different.
err = d.validateStateBeforeAcceptingRampingUpdate(args)
if err != nil {
return nil, err
}
prevRampingVersion := d.State.RoutingConfig.RampingVersion
prevRampingVersionPercentage := d.State.RoutingConfig.RampingVersionPercentage
newRampingVersion := args.Version
routingUpdateTime := timestamppb.New(workflow.Now(ctx))
var rampingSinceTime *timestamppb.Timestamp
var rampingVersionUpdateTime *timestamppb.Timestamp
// unsetting ramp
if newRampingVersion == "" {
unsetRampUpdateArgs := &deploymentspb.SyncVersionStateUpdateArgs{
RoutingUpdateTime: routingUpdateTime,
RampingSinceTime: nil, // remove ramp
RampPercentage: 0, // remove ramp
}
if prevRampingVersion != worker_versioning.UnversionedVersionId {
if _, err := d.syncVersion(ctx, prevRampingVersion, unsetRampUpdateArgs); err != nil {
return nil, err
}
} else {
if err := d.syncUnversionedRamp(ctx, unsetRampUpdateArgs); err != nil {
return nil, err
}
}
rampingVersionUpdateTime = routingUpdateTime // ramp was updated to ""
// Set summary drainage status immediately to draining.
// We know prevRampingVersion cannot have been current, so it must now be draining
d.setDrainageStatus(prevRampingVersion, enumspb.VERSION_DRAINAGE_STATUS_DRAINING)
} else {
// setting ramp
if prevRampingVersion == newRampingVersion { // the version was already ramping, user changing ramp %
rampingSinceTime = d.State.RoutingConfig.RampingVersionChangedTime
rampingVersionUpdateTime = d.State.RoutingConfig.RampingVersionChangedTime
} else {
// version ramping for the first time
currentVersion := d.State.RoutingConfig.CurrentVersion
if !args.IgnoreMissingTaskQueues &&
currentVersion != worker_versioning.UnversionedVersionId &&
newRampingVersion != worker_versioning.UnversionedVersionId {
isMissingTaskQueues, err := d.isVersionMissingTaskQueues(ctx, currentVersion, newRampingVersion)
if err != nil {
d.logger.Info("Error verifying poller presence in version", "error", err)
return nil, err
}
if isMissingTaskQueues {
return nil, serviceerror.NewFailedPrecondition("New ramping version does not have all the task queues from the previous current version and some missing task queues are active and would become unversioned after this operation")
}
}
rampingSinceTime = routingUpdateTime
rampingVersionUpdateTime = routingUpdateTime
// Erase summary drainage status immediately, so it is not draining/drained.
d.setDrainageStatus(newRampingVersion, enumspb.VERSION_DRAINAGE_STATUS_UNSPECIFIED)
}
setRampUpdateArgs := &deploymentspb.SyncVersionStateUpdateArgs{
RoutingUpdateTime: routingUpdateTime,
RampingSinceTime: rampingSinceTime,
RampPercentage: args.Percentage,
}
if newRampingVersion != worker_versioning.UnversionedVersionId {
if _, err := d.syncVersion(ctx, newRampingVersion, setRampUpdateArgs); err != nil {
return nil, err
}
} else {
if err := d.syncUnversionedRamp(ctx, setRampUpdateArgs); err != nil {
return nil, err
}
}
// tell previous ramping version, if present, that it's no longer ramping
if prevRampingVersion != "" && prevRampingVersion != newRampingVersion {
unsetRampUpdateArgs := &deploymentspb.SyncVersionStateUpdateArgs{
RoutingUpdateTime: routingUpdateTime,
RampingSinceTime: nil, // remove ramp
RampPercentage: 0, // remove ramp
}
if prevRampingVersion != worker_versioning.UnversionedVersionId {
if _, err := d.syncVersion(ctx, prevRampingVersion, unsetRampUpdateArgs); err != nil {
return nil, err
}
} else {
if err := d.syncUnversionedRamp(ctx, unsetRampUpdateArgs); err != nil {
return nil, err
}
}
// Set summary drainage status immediately to draining.
// We know prevRampingVersion cannot have been current, so it must now be draining
d.setDrainageStatus(prevRampingVersion, enumspb.VERSION_DRAINAGE_STATUS_DRAINING)
}
}
// update local state
d.State.RoutingConfig.RampingVersion = newRampingVersion
d.State.RoutingConfig.RampingVersionPercentage = args.Percentage
d.State.RoutingConfig.RampingVersionChangedTime = rampingVersionUpdateTime
d.State.ConflictToken, _ = routingUpdateTime.AsTime().MarshalBinary()
d.State.LastModifierIdentity = args.Identity
// update memo
if err = d.updateMemo(ctx); err != nil {
return nil, err
}
return &deploymentspb.SetRampingVersionResponse{
PreviousVersion: prevRampingVersion,
PreviousPercentage: prevRampingVersionPercentage,
ConflictToken: d.State.ConflictToken,
}, nil
}
func (d *WorkflowRunner) setDrainageStatus(version string, status enumspb.VersionDrainageStatus) {
if summary := d.State.GetVersions()[version]; summary != nil {
summary.DrainageStatus = status
}
}
func (d *WorkflowRunner) validateStateBeforeAcceptingDeleteVersion(args *deploymentspb.DeleteVersionArgs) error {
if _, ok := d.State.Versions[args.Version]; !ok {
return temporal.NewApplicationError("version not found in deployment", errVersionNotFound)
}
return nil
}
func (d *WorkflowRunner) validateDeleteVersion(args *deploymentspb.DeleteVersionArgs) error {
return d.validateStateBeforeAcceptingDeleteVersion(args)
}
func (d *WorkflowRunner) handleDeleteVersion(ctx workflow.Context, args *deploymentspb.DeleteVersionArgs) error {
// use lock to enforce only one update at a time
err := d.lock.Lock(ctx)
if err != nil {
d.logger.Error("Could not acquire workflow lock")
return serviceerror.NewDeadlineExceeded("Could not acquire workflow lock")
}
d.pendingUpdates++
defer func() {
d.pendingUpdates--
d.lock.Unlock()
}()
// Validating the state before starting the DeleteVersion operation. This is required due to the following reason:
// The validator accepts/rejects updates based on the state of the deployment workflow. Theoretically, two concurrent delete version requests
// might be accepted by the validator since the local state of the workflow contains the version which is requested to be deleted. Since this update handler
// enforces sequential updates, after the first update completes, the version will be removed from the local state of the deployment workflow. The second update,
// now already accepted by the validator, should now not be allowed to run since the initial workflow state is different.
err = d.validateStateBeforeAcceptingDeleteVersion(args)
if err != nil {
return err
}
// ask version to delete itself
activityCtx := workflow.WithActivityOptions(ctx, defaultActivityOptions)
var res deploymentspb.SyncVersionStateActivityResult
err = workflow.ExecuteActivity(activityCtx, d.a.DeleteWorkerDeploymentVersion, &deploymentspb.DeleteVersionActivityArgs{
Identity: args.Identity,
DeploymentName: d.DeploymentName,
Version: args.Version,
RequestId: uuid.New(),
SkipDrainage: args.SkipDrainage,
}).Get(ctx, &res)
if err != nil {
return err
}
// update local state
delete(d.State.Versions, args.Version)
d.State.LastModifierIdentity = args.Identity
// update memo
return d.updateMemo(ctx)
}
func (d *WorkflowRunner) validateStateBeforeAcceptingSetCurrent(args *deploymentspb.SetCurrentVersionArgs) error {
if d.State.RoutingConfig.CurrentVersion == args.Version && d.State.LastModifierIdentity == args.Identity {
return temporal.NewApplicationError("no change", errNoChangeType, d.State.ConflictToken)
}
if args.ConflictToken != nil && !bytes.Equal(args.ConflictToken, d.State.ConflictToken) {
return temporal.NewApplicationError("conflict token mismatch", errConflictTokenMismatchType)
}
if _, ok := d.State.Versions[args.Version]; !ok && args.Version != worker_versioning.UnversionedVersionId {
d.logger.Info("version not found in deployment")
return temporal.NewApplicationError("version not found in deployment", errVersionNotFound)
}
return nil
}
func (d *WorkflowRunner) validateSetCurrent(args *deploymentspb.SetCurrentVersionArgs) error {
return d.validateStateBeforeAcceptingSetCurrent(args)
}
func (d *WorkflowRunner) handleSetCurrent(ctx workflow.Context, args *deploymentspb.SetCurrentVersionArgs) (*deploymentspb.SetCurrentVersionResponse, error) {
// use lock to enforce only one update at a time
err := d.lock.Lock(ctx)
if err != nil {
d.logger.Error("Could not acquire workflow lock")
return nil, serviceerror.NewDeadlineExceeded("Could not acquire workflow lock")
}
d.pendingUpdates++
defer func() {
d.pendingUpdates--
d.lock.Unlock()
}()
// Validating the state before starting the SetCurrent operation. This is required due to the following reason:
// The validator accepts/rejects updates based on the state of the deployment workflow. Theoretically, two concurrent update requests
// might be accepted by the validator since the state of the workflow, at that point in time, is valid for the updates to take place. Since this update handler
// enforces sequential updates, after the first update completes, the local state of the deployment workflow will change. The second update,
// now already accepted by the validator, should now not be allowed to run since the state of the workflow is different.
err = d.validateStateBeforeAcceptingSetCurrent(args)
if err != nil {
return nil, err
}
prevCurrentVersion := d.State.RoutingConfig.CurrentVersion
newCurrentVersion := args.Version
updateTime := timestamppb.New(workflow.Now(ctx))
if !args.IgnoreMissingTaskQueues &&
prevCurrentVersion != worker_versioning.UnversionedVersionId &&
newCurrentVersion != worker_versioning.UnversionedVersionId {
isMissingTaskQueues, err := d.isVersionMissingTaskQueues(ctx, prevCurrentVersion, newCurrentVersion)
if err != nil {
d.logger.Info("Error verifying poller presence in version", "error", err)
return nil, err
}
if isMissingTaskQueues {
return nil, serviceerror.NewFailedPrecondition("New current version does not have all the task queues from the previous current version and some missing task queues are active and would become unversioned after this operation")
}
}
if newCurrentVersion != worker_versioning.UnversionedVersionId {
// Tell new current version that it's current
currUpdateArgs := &deploymentspb.SyncVersionStateUpdateArgs{
RoutingUpdateTime: updateTime,
CurrentSinceTime: updateTime,
RampingSinceTime: nil, // remove ramp for that version if it was ramping
RampPercentage: 0, // remove ramp for that version if it was ramping
}
if _, err := d.syncVersion(ctx, newCurrentVersion, currUpdateArgs); err != nil {
return nil, err
}
// Erase summary drainage status immediately (in case it was previously drained/draining)
d.setDrainageStatus(newCurrentVersion, enumspb.VERSION_DRAINAGE_STATUS_UNSPECIFIED)
} else if d.State.RoutingConfig.RampingVersion == worker_versioning.UnversionedVersionId {
// If the new current is unversioned, and it was previously ramping, we need to tell
// all the task queues with unversioned ramp that they no longer have unversioned ramp.
// The task queues with unversioned ramp are the task queues of the previous current version.
// TODO (Carly): Should we ban people from changing the task queues in the current version while they have an unversioned ramp?
unsetRampUpdateArgs := &deploymentspb.SyncVersionStateUpdateArgs{
RoutingUpdateTime: updateTime,
RampingSinceTime: nil, // remove ramp
RampPercentage: 0, // remove ramp
}
if err := d.syncUnversionedRamp(ctx, unsetRampUpdateArgs); err != nil {
return nil, err
}
}
// If the new current version is unversioned and there was no unversioned ramp, all we need to
// do is tell the previous current version that it is not current. Then, the task queues in the
// previous current version will have no current version and will become unversioned implicitly.
if prevCurrentVersion != worker_versioning.UnversionedVersionId {
// Tell previous current that it's no longer current
prevUpdateArgs := &deploymentspb.SyncVersionStateUpdateArgs{
RoutingUpdateTime: updateTime,
CurrentSinceTime: nil, // remove current
RampingSinceTime: nil, // no change, the prev current was not ramping
RampPercentage: 0, // no change, the prev current was not ramping
}
if _, err := d.syncVersion(ctx, prevCurrentVersion, prevUpdateArgs); err != nil {
return nil, err
}
// Set summary drainage status immediately to draining.
// We know prevCurrentVersion cannot have been ramping, so it must now be draining
d.setDrainageStatus(prevCurrentVersion, enumspb.VERSION_DRAINAGE_STATUS_DRAINING)
}
// If the previous current version was unversioned, there is nothing in the task queues
// to remove, because they were implicitly unversioned. We don't have to remove any
// unversioned ramps, because current and ramping cannot both be unversioned.
// update local state
d.State.RoutingConfig.CurrentVersion = args.Version
d.State.RoutingConfig.CurrentVersionChangedTime = updateTime
d.State.ConflictToken, _ = updateTime.AsTime().MarshalBinary()
d.State.LastModifierIdentity = args.Identity
// unset ramping version if it was set to current version
if d.State.RoutingConfig.CurrentVersion == d.State.RoutingConfig.RampingVersion {
d.State.RoutingConfig.RampingVersion = ""
d.State.RoutingConfig.RampingVersionPercentage = 0
d.State.RoutingConfig.RampingVersionChangedTime = updateTime // since ramp was removed
}
// update memo
if err = d.updateMemo(ctx); err != nil {
return nil, err
}
return &deploymentspb.SetCurrentVersionResponse{
PreviousVersion: prevCurrentVersion,
ConflictToken: d.State.ConflictToken,
}, nil
}
func (d *WorkflowRunner) validateAddVersionToWorkerDeployment(args *deploymentspb.AddVersionUpdateArgs) error {
if d.State.Versions == nil {
return nil
}
for _, v := range d.State.Versions {
if v.Version == args.Version {
return temporal.NewApplicationError("deployment version already registered", errVersionAlreadyExistsType)
}
}
return nil
}
func (d *WorkflowRunner) getMaxVersions(ctx workflow.Context) int {
getMaxVersionsInDeployment := func(ctx workflow.Context) interface{} {
return d.unsafeMaxVersion()
}
intEq := func(a, b interface{}) bool {
return a == b
}
var maxVersions int
if err := workflow.MutableSideEffect(ctx, "getMaxVersions", getMaxVersionsInDeployment, intEq).Get(&maxVersions); err != nil {
// This should not happen really. but just in case.
return defaultMaxVersions
}
return maxVersions
}
func (d *WorkflowRunner) handleAddVersionToWorkerDeployment(ctx workflow.Context, args *deploymentspb.AddVersionUpdateArgs) error {
d.pendingUpdates++
defer func() {
d.pendingUpdates--
}()
maxVersions := d.getMaxVersions(ctx)
if len(d.State.Versions) >= maxVersions {
err := d.tryDeleteVersion(ctx)
if err != nil {
return temporal.NewApplicationError(fmt.Sprintf("cannot add version, already at max versions %d", maxVersions), errTooManyVersions)
}
}
d.State.Versions[args.Version] = &deploymentspb.WorkerDeploymentVersionSummary{
Version: args.Version,
CreateTime: args.CreateTime,
}
return nil
}
func (d *WorkflowRunner) tryDeleteVersion(ctx workflow.Context) error {
var sortedSummaries []*deploymentspb.WorkerDeploymentVersionSummary
for _, k := range workflow.DeterministicKeys(d.State.Versions) {
s := d.State.Versions[k]
sortedSummaries = append(sortedSummaries, s)
}
slices.SortFunc(sortedSummaries, func(a, b *deploymentspb.WorkerDeploymentVersionSummary) int {
// sorts in ascending order.
// cmp(a, b) should return a negative number when a < b, a positive number when a > b,
// and zero when a == b or a and b are incomparable in the sense of a strict weak ordering.
if a.GetCreateTime().AsTime().After(b.GetCreateTime().AsTime()) {
return 1
} else if a.GetCreateTime().AsTime().Before(b.GetCreateTime().AsTime()) {
return -1
}
return 0
})
for _, v := range sortedSummaries {
// this might hang on the lock
err := d.handleDeleteVersion(ctx, &deploymentspb.DeleteVersionArgs{
Identity: "try-delete-for-add-version",
Version: v.Version,
})
if err == nil {
return nil
}
}
return serviceerror.NewFailedPrecondition("could not add version: too many versions in deployment and none are eligible for deletion")
}
func (d *WorkflowRunner) syncVersion(ctx workflow.Context, targetVersion string, versionUpdateArgs *deploymentspb.SyncVersionStateUpdateArgs) (*deploymentspb.VersionLocalState, error) {
activityCtx := workflow.WithActivityOptions(ctx, defaultActivityOptions)
var res deploymentspb.SyncVersionStateActivityResult
err := workflow.ExecuteActivity(activityCtx, d.a.SyncWorkerDeploymentVersion, &deploymentspb.SyncVersionStateActivityArgs{
DeploymentName: d.DeploymentName,
Version: targetVersion,
UpdateArgs: versionUpdateArgs,
RequestId: d.newUUID(ctx),
}).Get(ctx, &res)
return res.VersionState, err
}
func (d *WorkflowRunner) syncUnversionedRamp(ctx workflow.Context, versionUpdateArgs *deploymentspb.SyncVersionStateUpdateArgs) error {
activityCtx := workflow.WithActivityOptions(ctx, defaultActivityOptions)
var res deploymentspb.SyncUnversionedRampActivityResponse
err := workflow.ExecuteActivity(
activityCtx,
d.a.SyncUnversionedRamp,
&deploymentspb.SyncUnversionedRampActivityArgs{
CurrentVersion: d.State.RoutingConfig.CurrentVersion,
UpdateArgs: versionUpdateArgs,
}).Get(ctx, &res)
if err != nil {
return err
}
// check propagation
err = workflow.ExecuteActivity(
activityCtx,
d.a.CheckUnversionedRampUserDataPropagation,
&deploymentspb.CheckWorkerDeploymentUserDataPropagationRequest{
TaskQueueMaxVersions: res.TaskQueueMaxVersions,
}).Get(ctx, nil)
return err
}
func (d *WorkflowRunner) isVersionMissingTaskQueues(ctx workflow.Context, prevCurrentVersion string, newCurrentVersion string) (bool, error) {
activityCtx := workflow.WithActivityOptions(ctx, defaultActivityOptions)
var res deploymentspb.IsVersionMissingTaskQueuesResult
err := workflow.ExecuteActivity(activityCtx, d.a.IsVersionMissingTaskQueues, &deploymentspb.IsVersionMissingTaskQueuesArgs{
PrevCurrentVersion: prevCurrentVersion,
NewCurrentVersion: newCurrentVersion,
}).Get(ctx, &res)
return res.IsMissingTaskQueues, err
}
func (d *WorkflowRunner) newUUID(ctx workflow.Context) string {
var val string
_ = workflow.SideEffect(ctx, func(ctx workflow.Context) any {
return uuid.New()
}).Get(&val)
return val
}
func (d *WorkflowRunner) updateMemo(ctx workflow.Context) error {
return workflow.UpsertMemo(ctx, map[string]any{
WorkerDeploymentMemoField: &deploymentspb.WorkerDeploymentWorkflowMemo{
DeploymentName: d.DeploymentName,
CreateTime: d.State.CreateTime,
RoutingConfig: d.State.RoutingConfig,
},
})
}