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