Files
tasks/service/history/worker_versioning_util.go
c278efc8c3 worker-versioning GA: revision number to handle async workflow inconsistencies. (#8553)
## What changed?
- This PR adds revision number mechanics to handle task dispatch
inconsistencies that could arise since our versioning API's are becoming
eventually-consistent.

## Why?
- Making our versioning API's eventually consistent.

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

## Potential risks
- There are risks but they are gated by a dynamic config.

---

> [!NOTE]
> Adds revision-number semantics to task dispatch/versioning and
introduces a new per-deployment TQ user-data schema, updating APIs,
persistence, and server logic (DC-gated) with comprehensive tests.
> 
> - **APIs (proto changes)**:
> - Add `revision_number` to `taskqueue.v1.TaskVersionDirective` and
propagate `task_dispatch_revision_number` in History
`Record*TaskStarted` requests.
> - Matching `SyncDeploymentUserDataRequest`: new fields
`deployment_name`, `update_routing_config`, `upsert_versions_data`,
`forget_versions`; response includes `routing_config_changed`. Deprecate
old `update_version_data`/`forget_version` oneof.
> - Deployment API: add `WorkerDeploymentVersionData`; mark
`DeploymentVersionData` deprecated where applicable.
> - **Persistence (task queue user data)**:
> - Extend `DeploymentData` with `deployments_data` (map of deployment →
`WorkerDeploymentData` holding `RoutingConfig` and per-build version
data). Deprecate legacy `versions` and `unversioned_ramp_data` fields.
> - **Server logic**:
> - Matching/History: compute target version using revision numbers;
start workflow deployment transitions using revision-aware decisions;
support mixed old/new schemas when calculating current/ramping.
> - Matching: new helpers to migrate/clean old-format entries, apply
routing-config updates atomically, and prevent query blackholes using
status.
> - Task dispatch carries and records revision number; internal task
struct/plumbing updated.
> - **Dynamic config**:
> - Add `system.useRevisionNumberForWorkerVersioning` (plumbed through
History/Matching) to gate new behavior.
> - **Tests**:
> - Add/expand unit and functional tests covering new schema,
routing-config updates, revision-number behavior, and propagation.
> - **Misc**:
> - Describe/Stats paths updated to read both schemas; minor build/deps
update (api-go replace).
> 
> <sup>Written by [Cursor
Bugbot]() for commit
e84f4845067edbb8f65d2b7bac1f579b620db41c. This will update automatically
on new commits. Configure
[here]().</sup>

---------

Co-authored-by: ShahabT <[email protected]>
2025-11-10 15:11:38 -05:00

231 lines
7.8 KiB
Go

package history
import (
"context"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
persistencespb "go.temporal.io/server/api/persistence/v1"
taskqueuespb "go.temporal.io/server/api/taskqueue/v1"
"go.temporal.io/server/common"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/persistence"
"go.temporal.io/server/common/worker_versioning"
"go.temporal.io/server/service/history/consts"
historyi "go.temporal.io/server/service/history/interfaces"
"go.temporal.io/server/service/history/tasks"
"go.temporal.io/server/service/history/workflow/cache"
)
func updateIndependentActivityBuildId(
ctx context.Context,
task tasks.Task,
buildId string,
shardContext historyi.ShardContext,
transactionPolicy historyi.TransactionPolicy,
workflowCache cache.Cache,
metricsHandler metrics.Handler,
logger log.Logger,
) (retErr error) {
if buildId == "" {
// the task is sync-matched, or versioning is not enabled for this Task Queue
return nil
}
defer func() {
if retErr != nil {
// An independent task is scheduled but we could not persist the assigned build ID in mutable state.
// This is only an issue for user visibility: until the task is started user would not see the assigned
// build ID.
// Since it does not affect WF progress, just logging warn and skipping the error.
// TODO: let the error bubble up so the task is rescheduled and build ID is fully updated
logger.Warn("failed to update activity's assigned build ID", append(tasks.Tags(task), tag.Error(retErr))...)
}
retErr = nil
}()
// We should reach to this point only when all the following conditions are true:
// - assignment rules are added for the Task Queue (i.e. worker versioning is enabled)
// - sync-match did not happen for this task
// - the activity is on a different task queue (or otherwise asked to be independently assigned to a build ID)
weContext, release, err := getWorkflowExecutionContextForTask(ctx, shardContext, workflowCache, task)
if err != nil {
return err
}
defer func() {
release(retErr)
}()
var mutableState historyi.MutableState
var scheduledEventId int64
var taskVersion int64
switch t := task.(type) {
case *tasks.ActivityTask:
scheduledEventId = t.ScheduledEventID
taskVersion = t.Version
mutableState, err = loadMutableStateForTransferTask(ctx, shardContext, weContext, t, metricsHandler, logger)
case *tasks.ActivityRetryTimerTask:
scheduledEventId = t.EventID
taskVersion = t.Version
mutableState, err = loadMutableStateForTimerTask(ctx, shardContext, weContext, t, metricsHandler, logger)
default:
return serviceerror.NewInvalidArgument("task type not supported")
}
if err != nil {
return err
}
if mutableState == nil {
return consts.ErrWorkflowExecutionNotFound
}
ai, activityRunning := mutableState.GetActivityInfo(scheduledEventId)
if !activityRunning {
// We did not find the activity task, it means that it's already completed.
return nil
}
err = CheckTaskVersion(shardContext, logger, mutableState.GetNamespaceEntry(), ai.Version, taskVersion, task)
if err != nil {
return err
}
err = mutableState.UpdateActivity(ai.ScheduledEventId, func(activityInfo *persistencespb.ActivityInfo, _ historyi.MutableState) error {
activityInfo.BuildIdInfo = &persistencespb.ActivityInfo_LastIndependentlyAssignedBuildId{LastIndependentlyAssignedBuildId: buildId}
return nil
})
if err != nil {
return err
}
return weContext.UpdateWorkflowExecutionWithNew(
ctx,
shardContext,
persistence.UpdateWorkflowModeUpdateCurrent,
nil,
nil,
transactionPolicy,
nil,
)
}
// initializeWorkflowAssignedBuildId sets the wf assigned build ID after scheduling the first workflow task, based on
// the build ID returned by matching.
func initializeWorkflowAssignedBuildId(
ctx context.Context,
transferTask *tasks.WorkflowTask,
buildId string,
shardContext historyi.ShardContext,
transactionPolicy historyi.TransactionPolicy,
workflowCache cache.Cache,
metricsHandler metrics.Handler,
logger log.Logger,
) (retErr error) {
if buildId == "" {
// the task is sync-matched, or versioning is not enabled for this Task Queue
return nil
}
defer func() {
if retErr != nil {
// The first workflow task is scheduled but we could not persist the assigned build ID in mutable state.
// This is only an issue for user visibility: until the task is started user would not see the assigned
// build ID.
// Since it does not affect WF progress, just logging warn and skipping the error.
// TODO: let the error bubble up so the task is rescheduled and build ID is fully updated
logger.Error("failed to update workflow's assigned build ID", append(tasks.Tags(transferTask), tag.Error(retErr))...)
}
retErr = nil
}()
// We should reach to this point only when all the following conditions are true:
// - assignment rules are added for the Task Queue (i.e. worker versioning is enabled)
// - sync-match did not happen for this task
// - this is the first workflow task of the execution
// - the workflow has not inherited a build ID (for child WF or CaN)
weContext, release, err := getWorkflowExecutionContextForTask(ctx, shardContext, workflowCache, transferTask)
if err != nil {
return err
}
mutableState, err := loadMutableStateForTransferTask(ctx, shardContext, weContext, transferTask, metricsHandler, logger)
if err != nil {
release(err)
return err
}
defer func() {
release(retErr)
}()
if mutableState == nil {
return consts.ErrWorkflowExecutionNotFound
}
workflowTask := mutableState.GetWorkflowTaskByID(transferTask.ScheduledEventID)
if workflowTask == nil {
return nil
}
err = CheckTaskVersion(shardContext, logger, mutableState.GetNamespaceEntry(), workflowTask.Version, transferTask.Version, transferTask)
if err != nil {
return err
}
if mutableState.HasCompletedAnyWorkflowTask() || workflowTask.StartedEventID != common.EmptyEventID {
// workflow task is running or already completed. buildId is potentially stale and useless.
// workflow's assigned build ID should be already updated via RecordWorkflowTaskStarted
return nil
}
err = mutableState.UpdateBuildIdAssignment(buildId)
if err != nil {
return err
}
return weContext.UpdateWorkflowExecutionWithNew(
ctx,
shardContext,
persistence.UpdateWorkflowModeUpdateCurrent,
nil,
nil,
transactionPolicy,
nil,
)
}
func MakeDirectiveForWorkflowTask(ms historyi.MutableState) *taskqueuespb.TaskVersionDirective {
return worker_versioning.MakeDirectiveForWorkflowTask(
ms.GetInheritedBuildId(),
ms.GetAssignedBuildId(),
ms.GetMostRecentWorkerVersionStamp(),
ms.HasCompletedAnyWorkflowTask(),
ms.GetEffectiveVersioningBehavior(),
ms.GetEffectiveDeployment(),
ms.GetVersioningRevisionNumber(),
)
}
func MakeDirectiveForActivityTask(mutableState historyi.MutableState, activityInfo *persistencespb.ActivityInfo) *taskqueuespb.TaskVersionDirective {
if behavior := mutableState.GetEffectiveVersioningBehavior(); behavior != enumspb.VERSIONING_BEHAVIOR_UNSPECIFIED {
d := mutableState.GetEffectiveDeployment()
return &taskqueuespb.TaskVersionDirective{Behavior: behavior,
DeploymentVersion: worker_versioning.DeploymentVersionFromDeployment(d),
RevisionNumber: mutableState.GetVersioningRevisionNumber(),
}
}
if !activityInfo.UseCompatibleVersion && activityInfo.GetUseWorkflowBuildIdInfo() == nil {
return worker_versioning.MakeUseAssignmentRulesDirective()
} else if id := mutableState.GetAssignedBuildId(); id != "" {
return worker_versioning.MakeBuildIdDirective(id)
} else if id := worker_versioning.BuildIdIfUsingVersioning(mutableState.GetMostRecentWorkerVersionStamp()); id != "" {
// TODO: old versioning only [cleanup-old-wv]
return worker_versioning.MakeBuildIdDirective(id)
}
// else: unversioned execution
return nil
}