Files
tasks/service/matching/matching_engine.go
Stephan BehnkeandGitHub 137ddcbfbb GetWorkflowExecutionHistory long poll soft timeout (#8238)
## What changed?

Added a "soft timeout" (language used by Workflow Update) to
`GetWorkflowExecutionHistory` long polls.

## Why?

We don't want to terminate the long poll connection but instead keep it
alive by sending a response back just before the timeout. The idea is
that this will prevent connections from opening/terminating repeatedly
(ie connection churn).

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

## Potential risks

I was only able to verify this manually by forcing a timeout in the
server and verifying that instead of a deadline exceeded I saw a result.
I'll assume this will work since the existing code already tried doing
just exactly that, but it didn't do it well.
2025-08-27 01:28:26 +00:00

3055 lines
118 KiB
Go

package matching
import (
"bytes"
"context"
"errors"
"fmt"
"math"
"math/rand"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/nexus-rpc/sdk-go/nexus"
"github.com/pborman/uuid"
commonpb "go.temporal.io/api/common/v1"
deploymentpb "go.temporal.io/api/deployment/v1"
enumspb "go.temporal.io/api/enums/v1"
historypb "go.temporal.io/api/history/v1"
"go.temporal.io/api/serviceerror"
taskqueuepb "go.temporal.io/api/taskqueue/v1"
"go.temporal.io/api/workflowservice/v1"
enumsspb "go.temporal.io/server/api/enums/v1"
"go.temporal.io/server/api/historyservice/v1"
"go.temporal.io/server/api/matchingservice/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
replicationspb "go.temporal.io/server/api/replication/v1"
taskqueuespb "go.temporal.io/server/api/taskqueue/v1"
tokenspb "go.temporal.io/server/api/token/v1"
"go.temporal.io/server/common"
"go.temporal.io/server/common/backoff"
"go.temporal.io/server/common/clock"
hlc "go.temporal.io/server/common/clock/hybrid_logical_clock"
"go.temporal.io/server/common/cluster"
"go.temporal.io/server/common/collection"
"go.temporal.io/server/common/contextutil"
"go.temporal.io/server/common/headers"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/membership"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/namespace"
commonnexus "go.temporal.io/server/common/nexus"
"go.temporal.io/server/common/persistence"
"go.temporal.io/server/common/persistence/serialization"
"go.temporal.io/server/common/persistence/visibility/manager"
"go.temporal.io/server/common/primitives/timestamp"
"go.temporal.io/server/common/quotas"
"go.temporal.io/server/common/resource"
"go.temporal.io/server/common/searchattribute"
serviceerrors "go.temporal.io/server/common/serviceerror"
"go.temporal.io/server/common/stream_batcher"
"go.temporal.io/server/common/tasktoken"
"go.temporal.io/server/common/testing/testhooks"
"go.temporal.io/server/common/tqid"
"go.temporal.io/server/common/util"
"go.temporal.io/server/common/worker_versioning"
"go.temporal.io/server/service/history/api"
"go.temporal.io/server/service/worker/deployment"
"go.temporal.io/server/service/worker/workerdeployment"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
// If sticky poller is not seem in last 10s, we treat it as sticky worker unavailable
// This seems aggressive, but the default sticky schedule_to_start timeout is 5s, so 10s seems reasonable.
stickyPollerUnavailableWindow = 10 * time.Second
// If a compatible poller hasn't been seen for this time, we fail the CommitBuildId
// Set to 70s so that it's a little over the max time a poller should be kept waiting.
versioningPollerSeenWindow = 70 * time.Second
recordTaskStartedDefaultTimeout = 10 * time.Second
recordTaskStartedSyncMatchTimeout = 1 * time.Second
)
type (
TaskDispatchRateLimiter quotas.RequestRateLimiter
pollerIDCtxKey string
identityCtxKey string
taskQueueCounterKey struct {
namespaceID string
taskType enumspb.TaskQueueType
partitionType enumspb.TaskQueueKind
versioned string // one of these values: "unversioned", "versionSet", "buildId"
}
pollMetadata struct {
taskQueueMetadata *taskqueuepb.TaskQueueMetadata
workerVersionCapabilities *commonpb.WorkerVersionCapabilities
deploymentOptions *deploymentpb.WorkerDeploymentOptions
forwardedFrom string
localPollStartTime time.Time
}
userDataUpdate struct {
taskQueue string
update persistence.SingleTaskQueueUserDataUpdate
}
gaugeMetrics struct {
loadedTaskQueueFamilyCount map[taskQueueCounterKey]int
loadedTaskQueueCount map[taskQueueCounterKey]int
loadedTaskQueuePartitionCount map[taskQueueCounterKey]int
loadedPhysicalTaskQueueCount map[taskQueueCounterKey]int
lock sync.Mutex
}
// Implements matching.Engine
matchingEngineImpl struct {
status int32
taskManager persistence.TaskManager
fairTaskManager persistence.FairTaskManager
historyClient resource.HistoryClient
matchingRawClient resource.MatchingRawClient
deploymentStoreClient deployment.DeploymentStoreClient
workerDeploymentClient workerdeployment.Client
tokenSerializer *tasktoken.Serializer
historySerializer serialization.Serializer
logger log.Logger
throttledLogger log.ThrottledLogger
namespaceRegistry namespace.Registry
hostInfoProvider membership.HostInfoProvider
serviceResolver membership.ServiceResolver
membershipChangedCh chan *membership.ChangedEvent
clusterMeta cluster.Metadata
timeSource clock.TimeSource
visibilityManager manager.VisibilityManager
nexusEndpointClient *nexusEndpointClient
nexusEndpointsOwnershipLostCh chan struct{}
saMapperProvider searchattribute.MapperProvider
saProvider searchattribute.Provider
metricsHandler metrics.Handler
partitionsLock sync.RWMutex // locks mutation of partitions
partitions map[tqid.PartitionKey]taskQueuePartitionManager
gaugeMetrics gaugeMetrics // per-namespace task queue counters
config *Config
versionChecker headers.VersionChecker
testHooks testhooks.TestHooks
// queryResults maps query TaskID (which is a UUID generated in QueryWorkflow() call) to a channel
// that QueryWorkflow() will block on. The channel is unblocked either by worker sending response through
// RespondQueryTaskCompleted() or through an internal service error causing temporal to be unable to dispatch
// query task to workflow worker.
queryResults collection.SyncMap[string, chan *queryResult]
// nexusResults maps nexus TaskID (which is a UUID generated in the DispatchNexusTask() call) to
// a channel that DispatchNexusTask() blocks on. The channel is unblocked either by worker responding
// via RespondNexusTaskCompleted() or RespondNexusTaskFailed(), or through an internal service error.
nexusResults collection.SyncMap[string, chan *nexusResult]
// outstandingPollers is needed to keep track of all outstanding pollers for a particular
// taskqueue. PollerID generated by frontend is used as the key and CancelFunc is the
// value. This is used to cancel the context to unblock any outstanding poller when
// the frontend detects client connection is closed to prevent tasks being dispatched
// to zombie pollers.
outstandingPollers collection.SyncMap[string, context.CancelFunc]
// Only set if global namespaces are enabled on the cluster.
namespaceReplicationQueue persistence.NamespaceReplicationQueue
// Lock to serialize replication queue updates.
replicationLock sync.Mutex
// Serialize and batch user data updates by namespace.
userDataUpdateBatchers collection.SyncMap[namespace.ID, *stream_batcher.Batcher[*userDataUpdate, error]]
// Stores results of reachability queries to visibility
reachabilityCache reachabilityCache
// Rate limiter to limit the task dispatch
rateLimiter TaskDispatchRateLimiter
}
)
var (
// EmptyPollWorkflowTaskQueueResponse is the response when there are no workflow tasks to hand out
emptyPollWorkflowTaskQueueResponse = &matchingservice.PollWorkflowTaskQueueResponse{}
// EmptyPollActivityTaskQueueResponse is the response when there are no activity tasks to hand out
emptyPollActivityTaskQueueResponse = &matchingservice.PollActivityTaskQueueResponse{}
errNoTasks = errors.New("no tasks")
pollerIDKey pollerIDCtxKey = "pollerID"
identityKey identityCtxKey = "identity"
// The routing key for the single partition used to route Nexus endpoints CRUD RPCs to.
nexusEndpointsTablePartitionRoutingKey = tqid.MustNormalPartitionFromRpcName("not-applicable", "not-applicable", enumspb.TASK_QUEUE_TYPE_UNSPECIFIED).RoutingKey()
// Options for batching user data updates.
userDataBatcherOptions = stream_batcher.BatcherOptions{
MaxItems: 100,
MinDelay: 100 * time.Millisecond,
MaxDelay: 500 * time.Millisecond,
IdleTime: time.Minute,
}
)
var _ Engine = (*matchingEngineImpl)(nil) // Asserts that interface is indeed implemented
// NewEngine creates an instance of matching engine
func NewEngine(
taskManager persistence.TaskManager,
fairTaskManager persistence.FairTaskManager,
historyClient resource.HistoryClient,
matchingRawClient resource.MatchingRawClient,
deploymentStoreClient deployment.DeploymentStoreClient, // [wv-cleanup-pre-release]
workerDeploymentClient workerdeployment.Client,
config *Config,
logger log.Logger,
throttledLogger log.ThrottledLogger,
metricsHandler metrics.Handler,
namespaceRegistry namespace.Registry,
hostInfoProvider membership.HostInfoProvider,
resolver membership.ServiceResolver,
clusterMeta cluster.Metadata,
namespaceReplicationQueue persistence.NamespaceReplicationQueue,
visibilityManager manager.VisibilityManager,
nexusEndpointManager persistence.NexusEndpointManager,
testHooks testhooks.TestHooks,
saProvider searchattribute.Provider,
saMapperProvider searchattribute.MapperProvider,
rateLimiter TaskDispatchRateLimiter,
) Engine {
scopedMetricsHandler := metricsHandler.WithTags(metrics.OperationTag(metrics.MatchingEngineScope))
e := &matchingEngineImpl{
status: common.DaemonStatusInitialized,
taskManager: taskManager,
fairTaskManager: fairTaskManager,
historyClient: historyClient,
matchingRawClient: matchingRawClient,
deploymentStoreClient: deploymentStoreClient,
tokenSerializer: tasktoken.NewSerializer(),
workerDeploymentClient: workerDeploymentClient,
historySerializer: serialization.NewSerializer(),
logger: log.With(logger, tag.ComponentMatchingEngine),
throttledLogger: log.With(throttledLogger, tag.ComponentMatchingEngine),
namespaceRegistry: namespaceRegistry,
hostInfoProvider: hostInfoProvider,
serviceResolver: resolver,
membershipChangedCh: make(chan *membership.ChangedEvent, 1), // allow one signal to be buffered while we're working
clusterMeta: clusterMeta,
timeSource: clock.NewRealTimeSource(), // No need to mock this at the moment
visibilityManager: visibilityManager,
nexusEndpointClient: newEndpointClient(config.NexusEndpointsRefreshInterval, nexusEndpointManager),
nexusEndpointsOwnershipLostCh: make(chan struct{}),
saProvider: saProvider,
saMapperProvider: saMapperProvider,
metricsHandler: scopedMetricsHandler,
partitions: make(map[tqid.PartitionKey]taskQueuePartitionManager),
gaugeMetrics: gaugeMetrics{
loadedTaskQueueFamilyCount: make(map[taskQueueCounterKey]int),
loadedTaskQueueCount: make(map[taskQueueCounterKey]int),
loadedTaskQueuePartitionCount: make(map[taskQueueCounterKey]int),
loadedPhysicalTaskQueueCount: make(map[taskQueueCounterKey]int),
},
config: config,
versionChecker: headers.NewDefaultVersionChecker(),
testHooks: testHooks,
queryResults: collection.NewSyncMap[string, chan *queryResult](),
nexusResults: collection.NewSyncMap[string, chan *nexusResult](),
outstandingPollers: collection.NewSyncMap[string, context.CancelFunc](),
namespaceReplicationQueue: namespaceReplicationQueue,
userDataUpdateBatchers: collection.NewSyncMap[namespace.ID, *stream_batcher.Batcher[*userDataUpdate, error]](),
rateLimiter: rateLimiter,
}
e.reachabilityCache = newReachabilityCache(
metrics.NoopMetricsHandler,
visibilityManager,
e.config.ReachabilityCacheOpenWFsTTL(),
e.config.ReachabilityCacheClosedWFsTTL())
return e
}
func (e *matchingEngineImpl) Start() {
if !atomic.CompareAndSwapInt32(
&e.status,
common.DaemonStatusInitialized,
common.DaemonStatusStarted,
) {
return
}
go e.watchMembership()
_ = e.serviceResolver.AddListener(e.listenerKey(), e.membershipChangedCh)
}
func (e *matchingEngineImpl) Stop() {
if !atomic.CompareAndSwapInt32(
&e.status,
common.DaemonStatusStarted,
common.DaemonStatusStopped,
) {
return
}
_ = e.serviceResolver.RemoveListener(e.listenerKey())
close(e.membershipChangedCh)
e.nexusEndpointClient.notifyOwnershipChanged(false)
for _, l := range e.getTaskQueuePartitions(math.MaxInt32) {
l.Stop(unloadCauseShuttingDown)
}
}
func (e *matchingEngineImpl) listenerKey() string {
return fmt.Sprintf("matchingEngine[%p]", e)
}
func (e *matchingEngineImpl) watchMembership() {
self := e.hostInfoProvider.HostInfo().Identity()
for range e.membershipChangedCh {
delay := e.config.MembershipUnloadDelay()
if delay == 0 {
continue
}
e.notifyNexusEndpointsOwnershipChange()
// Check all our loaded partitions to see if we lost ownership of any of them.
e.partitionsLock.RLock()
partitions := make([]tqid.Partition, 0, len(e.partitions))
for _, pm := range e.partitions {
partitions = append(partitions, pm.Partition())
}
e.partitionsLock.RUnlock()
partitions = util.FilterSlice(partitions, func(p tqid.Partition) bool {
owner, err := e.serviceResolver.Lookup(p.RoutingKey())
return err == nil && owner.Identity() != self
})
const batchSize = 100
for i := 0; i < len(partitions); i += batchSize {
// We don't own these anymore, but don't unload them immediately, wait a few seconds to ensure
// the membership update has propagated everywhere so that they won't get immediately re-loaded.
// Note that we don't verify ownership at load time, so this is the only guard against a task
// queue bouncing back and forth due to long membership propagation time.
batch := partitions[i:min(len(partitions), i+batchSize)]
wait := backoff.Jitter(delay, 0.1)
time.AfterFunc(wait, func() {
// maybe the whole engine stopped
if atomic.LoadInt32(&e.status) != common.DaemonStatusStarted {
return
}
for _, p := range batch {
// maybe ownership changed again
owner, err := e.serviceResolver.Lookup(p.RoutingKey())
if err != nil || owner.Identity() == self {
return
}
// now we can unload
e.unloadTaskQueuePartitionByKey(p, nil, unloadCauseMembership)
}
})
}
}
}
func (e *matchingEngineImpl) getTaskQueuePartitions(maxCount int) (lists []taskQueuePartitionManager) {
e.partitionsLock.RLock()
defer e.partitionsLock.RUnlock()
lists = make([]taskQueuePartitionManager, 0, len(e.partitions))
count := 0
for _, tlMgr := range e.partitions {
lists = append(lists, tlMgr)
count++
if count >= maxCount {
break
}
}
return
}
// Returns taskQueuePartitionManager for a task queue. If not already cached, and create is true, tries
// to get new range from DB and create one. This blocks (up to the context deadline) for the
// task queue to be initialized.
//
// Note that task queue kind (sticky vs normal) and normal name for sticky task queues is not used as
// part of the task queue identity. That means that if getTaskQueuePartitionManager
// is called twice with the same task queue but different sticky info, the
// properties of the taskQueuePartitionManager will depend on which call came first. In general, we can
// rely on kind being the same for all calls now, but normalName was a later addition to the
// protocol and is not always set consistently. normalName is only required when using
// versioning, and SDKs that support versioning will always set it. The current server version
// will also set it when adding tasks from history. So that particular inconsistency is okay.
func (e *matchingEngineImpl) getTaskQueuePartitionManager(
ctx context.Context,
partition tqid.Partition,
create bool,
loadCause loadCause,
) (retPM taskQueuePartitionManager, retCreated bool, retErr error) {
var newPM *taskQueuePartitionManagerImpl
defer func() {
if retErr != nil || retPM == nil {
return
}
if retErr = retPM.WaitUntilInitialized(ctx); retErr != nil {
e.unloadTaskQueuePartition(retPM, unloadCauseInitError)
return
}
if retCreated {
// Whenever a root partition is loaded, we need to force all child partitions to load.
// If there is a backlog of tasks on any child partitions, force loading will ensure
// that they can forward their tasks the poller which caused the root partition to be
// loaded. These partitions could be managed by this matchingEngineImpl, but are most
// likely not. We skip checking and just make gRPC requests to force loading them all.
// Note that if retCreated is true, retPM must be newPM, so we can use newPM here.
newPM.ForceLoadAllChildPartitions()
}
}()
key := partition.Key()
e.partitionsLock.RLock()
pm, ok := e.partitions[key]
e.partitionsLock.RUnlock()
if ok {
return pm, false, nil
}
if !create {
return nil, false, nil
}
namespaceEntry, err := e.namespaceRegistry.GetNamespaceByID(namespace.ID(partition.NamespaceId()))
if err != nil {
return nil, false, err
}
nsName := namespaceEntry.Name()
tqConfig := newTaskQueueConfig(partition.TaskQueue(), e.config, nsName)
tqConfig.loadCause = loadCause
logger, throttledLogger, metricsHandler := e.loggerAndMetricsForPartition(nsName, partition, tqConfig)
onFatalErr := func(cause unloadCause) { newPM.unloadFromEngine(cause) }
onUserDataChanged := func() { newPM.userDataChanged() }
userDataManager := newUserDataManager(
e.taskManager,
e.matchingRawClient,
onFatalErr,
onUserDataChanged,
partition,
tqConfig,
logger,
e.namespaceRegistry,
)
newPM, err = newTaskQueuePartitionManager(
e,
namespaceEntry,
partition,
tqConfig,
logger,
throttledLogger,
metricsHandler,
userDataManager,
)
if err != nil {
return nil, false, err
}
// If it gets here, write lock and check again in case a task queue is created between the two locks
e.partitionsLock.Lock()
pm, ok = e.partitions[key]
if ok {
e.partitionsLock.Unlock()
return pm, false, nil
}
e.partitions[key] = newPM
e.partitionsLock.Unlock()
newPM.Start()
return newPM, true, nil
}
func (e *matchingEngineImpl) loggerAndMetricsForPartition(
nsName namespace.Name,
partition tqid.Partition,
tqConfig *taskQueueConfig,
) (log.Logger, log.Logger, metrics.Handler) {
logger := log.With(e.logger,
tag.WorkflowTaskQueueName(partition.RpcName()),
tag.WorkflowTaskQueueType(partition.TaskType()),
tag.WorkflowNamespace(nsName.String()))
throttledLogger := log.With(e.throttledLogger,
tag.WorkflowTaskQueueName(partition.RpcName()),
tag.WorkflowTaskQueueType(partition.TaskType()),
tag.WorkflowNamespace(nsName.String()))
metricsHandler := metrics.GetPerTaskQueuePartitionIDScope(
e.metricsHandler,
nsName.String(),
partition,
tqConfig.BreakdownMetricsByTaskQueue(),
tqConfig.BreakdownMetricsByPartition(),
metrics.OperationTag(metrics.MatchingTaskQueuePartitionManagerScope),
)
return logger, throttledLogger, metricsHandler
}
// For use in tests
func (e *matchingEngineImpl) updateTaskQueue(partition tqid.Partition, mgr taskQueuePartitionManager) {
e.partitionsLock.Lock()
defer e.partitionsLock.Unlock()
e.partitions[partition.Key()] = mgr
}
// AddWorkflowTask either delivers task directly to waiting poller or saves it into task queue persistence.
func (e *matchingEngineImpl) AddWorkflowTask(
ctx context.Context,
addRequest *matchingservice.AddWorkflowTaskRequest,
) (buildId string, syncMatch bool, err error) {
partition, err := tqid.PartitionFromProto(addRequest.TaskQueue, addRequest.NamespaceId, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
if err != nil {
return "", false, err
}
sticky := partition.Kind() == enumspb.TASK_QUEUE_KIND_STICKY
// do not load sticky task queue if it is not already loaded, which means it has no poller.
pm, _, err := e.getTaskQueuePartitionManager(ctx, partition, !sticky, loadCauseTask)
if err != nil {
return "", false, err
} else if sticky && !stickyWorkerAvailable(pm) {
return "", false, serviceerrors.NewStickyWorkerUnavailable()
}
// This needs to move to history see - https://go.temporal.io/server/issues/181
var expirationTime *timestamppb.Timestamp
now := time.Now().UTC()
expirationDuration := addRequest.GetScheduleToStartTimeout().AsDuration()
if expirationDuration != 0 {
expirationTime = timestamppb.New(now.Add(expirationDuration))
}
taskInfo := &persistencespb.TaskInfo{
NamespaceId: addRequest.NamespaceId,
RunId: addRequest.Execution.GetRunId(),
WorkflowId: addRequest.Execution.GetWorkflowId(),
ScheduledEventId: addRequest.GetScheduledEventId(),
Clock: addRequest.GetClock(),
ExpiryTime: expirationTime,
CreateTime: timestamppb.New(now),
VersionDirective: addRequest.VersionDirective,
Priority: addRequest.Priority,
}
return pm.AddTask(ctx, addTaskParams{
taskInfo: taskInfo,
forwardInfo: addRequest.ForwardInfo,
})
}
// AddActivityTask either delivers task directly to waiting poller or save it into task queue persistence.
func (e *matchingEngineImpl) AddActivityTask(
ctx context.Context,
addRequest *matchingservice.AddActivityTaskRequest,
) (buildId string, syncMatch bool, err error) {
partition, err := tqid.PartitionFromProto(addRequest.TaskQueue, addRequest.GetNamespaceId(), enumspb.TASK_QUEUE_TYPE_ACTIVITY)
if err != nil {
return "", false, err
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, partition, true, loadCauseTask)
if err != nil {
return "", false, err
}
var expirationTime *timestamppb.Timestamp
now := time.Now().UTC()
expirationDuration := timestamp.DurationValue(addRequest.GetScheduleToStartTimeout())
if expirationDuration != 0 {
expirationTime = timestamppb.New(now.Add(expirationDuration))
}
taskInfo := &persistencespb.TaskInfo{
NamespaceId: addRequest.NamespaceId,
RunId: addRequest.Execution.GetRunId(),
WorkflowId: addRequest.Execution.GetWorkflowId(),
ScheduledEventId: addRequest.GetScheduledEventId(),
Clock: addRequest.GetClock(),
CreateTime: timestamppb.New(now),
ExpiryTime: expirationTime,
VersionDirective: addRequest.VersionDirective,
Stamp: addRequest.Stamp,
Priority: addRequest.Priority,
}
return pm.AddTask(ctx, addTaskParams{
taskInfo: taskInfo,
forwardInfo: addRequest.ForwardInfo,
})
}
// PollWorkflowTaskQueue tries to get the workflow task using exponential backoff.
func (e *matchingEngineImpl) PollWorkflowTaskQueue(
ctx context.Context,
req *matchingservice.PollWorkflowTaskQueueRequest,
opMetrics metrics.Handler,
) (*matchingservice.PollWorkflowTaskQueueResponse, error) {
namespaceID := namespace.ID(req.GetNamespaceId())
pollerID := req.GetPollerId()
request := req.PollRequest
taskQueueName := request.TaskQueue.GetName()
// Namespace field is not populated for forwarded requests.
if len(request.Namespace) == 0 {
ns, err := e.namespaceRegistry.GetNamespaceName(namespace.ID(req.GetNamespaceId()))
if err != nil {
return nil, err
}
request.Namespace = ns.String()
}
pollLoop:
for {
err := common.IsValidContext(ctx)
if err != nil {
return nil, err
}
// Add frontend generated pollerID to context so taskqueueMgr can support cancellation of
// long-poll when frontend calls CancelOutstandingPoll API
pollerCtx := context.WithValue(ctx, pollerIDKey, pollerID)
pollerCtx = context.WithValue(pollerCtx, identityKey, request.GetIdentity())
partition, err := tqid.PartitionFromProto(request.TaskQueue, req.NamespaceId, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
if err != nil {
return nil, err
}
pollMetadata := &pollMetadata{
workerVersionCapabilities: request.WorkerVersionCapabilities,
deploymentOptions: request.DeploymentOptions,
forwardedFrom: req.GetForwardedSource(),
}
task, versionSetUsed, err := e.pollTask(pollerCtx, partition, pollMetadata)
if err != nil {
if errors.Is(err, errNoTasks) {
return emptyPollWorkflowTaskQueueResponse, nil
}
return nil, err
}
if task.isStarted() {
// tasks received from remote are already started. So, simply forward the response
return task.pollWorkflowTaskQueueResponse(), nil
}
if task.isQuery() {
task.finish(nil, true) // this only means query task sync match succeed.
// for query task, we don't need to update history to record workflow task started. but we need to know
// the NextEventID and the currently set sticky task queue.
// TODO: in theory we only need this lookup for non-sticky queries (to get NextEventID for populating
// partial history in the response), but we need a new history API to determine whether the query
// is sticky or not without this call
mutableStateResp, err := e.historyClient.GetMutableState(ctx, &historyservice.GetMutableStateRequest{
NamespaceId: req.GetNamespaceId(),
Execution: task.workflowExecution(),
})
if err != nil {
// will notify query client that the query task failed
_ = e.deliverQueryResult(task.query.taskID, &queryResult{internalError: err})
return emptyPollWorkflowTaskQueueResponse, nil
}
// A non-sticky poll may get task for a workflow that has sticky still set in its mutable state after
// their sticky worker is dead for longer than 10s. In such case, we should set this to false so that
// we return full history.
isStickyEnabled := taskQueueName == mutableStateResp.StickyTaskQueue.GetName()
hist, nextPageToken, err := e.getHistoryForQueryTask(ctx, namespaceID, task, isStickyEnabled)
if err != nil {
// will notify query client that the query task failed
_ = e.deliverQueryResult(task.query.taskID, &queryResult{internalError: err})
return emptyPollWorkflowTaskQueueResponse, nil
}
resp := &historyservice.RecordWorkflowTaskStartedResponse{
PreviousStartedEventId: mutableStateResp.PreviousStartedEventId,
NextEventId: mutableStateResp.NextEventId,
WorkflowType: mutableStateResp.WorkflowType,
StickyExecutionEnabled: isStickyEnabled,
WorkflowExecutionTaskQueue: mutableStateResp.TaskQueue,
BranchToken: mutableStateResp.CurrentBranchToken,
StartedEventId: common.EmptyEventID,
Attempt: 1,
History: hist,
NextPageToken: nextPageToken,
}
return e.createPollWorkflowTaskQueueResponse(task, resp, opMetrics), nil
}
requestClone := request
if versionSetUsed {
// We remove build ID from workerVersionCapabilities so History can differentiate between
// old and new versioning in Record*TaskStart.
// TODO: remove this block after old versioning cleanup. [cleanup-old-wv]
requestClone = common.CloneProto(request)
requestClone.WorkerVersionCapabilities.BuildId = ""
}
resp, err := e.recordWorkflowTaskStarted(ctx, requestClone, task)
if err != nil {
switch err := err.(type) {
case *serviceerror.Internal, *serviceerror.DataLoss:
e.nonRetryableErrorsDropTask(task, taskQueueName, err)
// drop the task as otherwise task would be stuck in a retry-loop
task.finish(nil, false)
case *serviceerror.NotFound: // mutable state not found, workflow not running or workflow task not found
e.logger.Info("Workflow task not found",
tag.WorkflowTaskQueueName(taskQueueName),
tag.WorkflowNamespaceID(task.event.Data.GetNamespaceId()),
tag.WorkflowID(task.event.Data.GetWorkflowId()),
tag.WorkflowRunID(task.event.Data.GetRunId()),
tag.TaskID(task.event.GetTaskId()),
tag.TaskVisibilityTimestamp(timestamp.TimeValue(task.event.Data.GetCreateTime())),
tag.WorkflowEventID(task.event.Data.GetScheduledEventId()),
tag.Error(err),
)
task.finish(nil, false)
case *serviceerrors.TaskAlreadyStarted:
e.logger.Debug("Duplicated workflow task", tag.WorkflowTaskQueueName(taskQueueName), tag.TaskID(task.event.GetTaskId()))
task.finish(nil, false)
case *serviceerrors.ObsoleteDispatchBuildId:
// history should've scheduled another task on the right build ID. dropping this one.
e.logger.Info("dropping workflow task due to invalid build ID",
tag.WorkflowTaskQueueName(taskQueueName),
tag.WorkflowNamespaceID(task.event.Data.GetNamespaceId()),
tag.WorkflowID(task.event.Data.GetWorkflowId()),
tag.WorkflowRunID(task.event.Data.GetRunId()),
tag.TaskID(task.event.GetTaskId()),
tag.TaskVisibilityTimestamp(timestamp.TimeValue(task.event.Data.GetCreateTime())),
tag.BuildId(requestClone.WorkerVersionCapabilities.GetBuildId()),
)
task.finish(nil, false)
case *serviceerrors.ObsoleteMatchingTask:
// History should've scheduled another task on the right task queue and deployment.
// Dropping this one.
e.logger.Info("dropping obsolete workflow task",
tag.WorkflowTaskQueueName(taskQueueName),
tag.WorkflowNamespaceID(task.event.Data.GetNamespaceId()),
tag.WorkflowID(task.event.Data.GetWorkflowId()),
tag.WorkflowRunID(task.event.Data.GetRunId()),
tag.TaskID(task.event.GetTaskId()),
tag.WorkflowScheduledEventID(task.event.Data.GetScheduledEventId()),
tag.TaskVisibilityTimestamp(timestamp.TimeValue(task.event.Data.GetCreateTime())),
tag.VersioningBehavior(task.event.Data.VersionDirective.GetBehavior()),
//nolint:staticcheck // SA1019 deprecated WorkerVersionCapabilities will clean up later
tag.Deployment(worker_versioning.DeploymentNameFromCapabilities(requestClone.WorkerVersionCapabilities, requestClone.DeploymentOptions)),
//nolint:staticcheck // SA1019 deprecated WorkerVersionCapabilities will clean up later
tag.BuildId(worker_versioning.BuildIdFromCapabilities(requestClone.WorkerVersionCapabilities, requestClone.DeploymentOptions)),
tag.Error(err),
)
task.finish(nil, false)
case *serviceerror.ResourceExhausted:
// If history returns one ResourceExhausted, it's likely to return more if we retry
// immediately. Instead, return the error to the client which will back off.
// BUSY_WORKFLOW is limited to one workflow and is okay to retry.
task.finish(err, false)
if err.Cause != enumspb.RESOURCE_EXHAUSTED_CAUSE_BUSY_WORKFLOW {
return nil, err
}
default:
task.finish(err, false)
if err.Error() == common.ErrNamespaceHandover.Error() {
// do not keep polling new tasks when namespace is in handover state
// as record start request will be rejected by history service
return nil, err
}
}
continue pollLoop
}
task.finish(nil, true)
return e.createPollWorkflowTaskQueueResponse(task, resp, opMetrics), nil
}
}
// getHistoryForQueryTask retrieves history associated with a query task returned
// by PollWorkflowTaskQueue. Returns empty history for sticky query and full history for non-sticky
func (e *matchingEngineImpl) getHistoryForQueryTask(
ctx context.Context,
nsID namespace.ID,
task *internalTask,
isStickyEnabled bool,
) (*historypb.History, []byte, error) {
if isStickyEnabled {
return &historypb.History{Events: []*historypb.HistoryEvent{}}, nil, nil
}
maxPageSize := int32(e.config.HistoryMaxPageSize(task.namespace.String()))
resp, err := e.historyClient.GetWorkflowExecutionHistory(ctx,
&historyservice.GetWorkflowExecutionHistoryRequest{
NamespaceId: nsID.String(),
Request: &workflowservice.GetWorkflowExecutionHistoryRequest{
Namespace: task.namespace.String(),
Execution: task.workflowExecution(),
MaximumPageSize: maxPageSize,
WaitNewEvent: false,
SkipArchival: true,
},
})
if err != nil {
return nil, nil, err
}
// History service can send history events in response.History.Events. In that case use that directly.
// This happens when history.sendRawHistoryBetweenInternalServices is enabled.
ns, err := e.namespaceRegistry.GetNamespaceName(nsID)
if err != nil {
return nil, nil, err
}
err = api.ProcessInternalRawHistory(
ctx,
e.saProvider,
e.saMapperProvider,
resp,
e.visibilityManager,
e.versionChecker,
ns,
false,
)
hist := resp.GetResponse().GetHistory()
if resp.GetResponse().GetRawHistory() != nil {
historyEvents := make([]*historypb.HistoryEvent, 0, maxPageSize)
for _, blob := range resp.GetResponse().GetRawHistory() {
events, err := e.historySerializer.DeserializeEvents(blob)
if err != nil {
return nil, nil, err
}
historyEvents = append(historyEvents, events...)
}
hist = &historypb.History{Events: historyEvents}
}
return hist, resp.GetResponse().GetNextPageToken(), err
}
func (e *matchingEngineImpl) nonRetryableErrorsDropTask(task *internalTask, taskQueueName string, err error) {
e.logger.Error("dropping task due to non-nonretryable errors",
tag.WorkflowNamespace(task.namespace.String()),
tag.WorkflowNamespaceID(task.event.Data.GetNamespaceId()),
tag.WorkflowID(task.event.Data.GetWorkflowId()),
tag.WorkflowRunID(task.event.Data.GetRunId()),
tag.WorkflowTaskQueueName(taskQueueName),
tag.TaskID(task.event.GetTaskId()),
tag.WorkflowScheduledEventID(task.event.Data.GetScheduledEventId()),
tag.Error(err),
tag.ErrorType(err),
)
metrics.NonRetryableTasks.With(e.metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err))
}
// PollActivityTaskQueue takes one task from the task manager, update workflow execution history, mark task as
// completed and return it to user. If a task from task manager is already started, return an empty response, without
// error. Timeouts handled by the timer queue.
func (e *matchingEngineImpl) PollActivityTaskQueue(
ctx context.Context,
req *matchingservice.PollActivityTaskQueueRequest,
opMetrics metrics.Handler,
) (*matchingservice.PollActivityTaskQueueResponse, error) {
pollerID := req.GetPollerId()
request := req.PollRequest
taskQueueName := request.TaskQueue.GetName()
// Namespace field is not populated for forwarded requests.
if len(request.Namespace) == 0 {
ns, err := e.namespaceRegistry.GetNamespaceName(namespace.ID(req.GetNamespaceId()))
if err != nil {
return nil, err
}
request.Namespace = ns.String()
}
pollLoop:
for {
err := common.IsValidContext(ctx)
if err != nil {
return nil, err
}
partition, err := tqid.PartitionFromProto(request.TaskQueue, req.NamespaceId, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
if err != nil {
return nil, err
}
// Add frontend generated pollerID to context so taskqueueMgr can support cancellation of
// long-poll when frontend calls CancelOutstandingPoll API
pollerCtx := context.WithValue(ctx, pollerIDKey, pollerID)
pollerCtx = context.WithValue(pollerCtx, identityKey, request.GetIdentity())
pollMetadata := &pollMetadata{
taskQueueMetadata: request.TaskQueueMetadata,
workerVersionCapabilities: request.WorkerVersionCapabilities,
deploymentOptions: request.DeploymentOptions,
forwardedFrom: req.GetForwardedSource(),
}
task, versionSetUsed, err := e.pollTask(pollerCtx, partition, pollMetadata)
if err != nil {
if errors.Is(err, errNoTasks) {
return emptyPollActivityTaskQueueResponse, nil
}
return nil, err
}
if task.isStarted() {
// tasks received from remote are already started. So, simply forward the response
return task.pollActivityTaskQueueResponse(), nil
}
requestClone := request
if versionSetUsed {
// We remove build ID from workerVersionCapabilities so History can differentiate between
// old and new versioning in Record*TaskStart.
// TODO: remove this block after old versioning cleanup. [cleanup-old-wv]
requestClone = common.CloneProto(request)
requestClone.WorkerVersionCapabilities.BuildId = ""
}
resp, err := e.recordActivityTaskStarted(ctx, requestClone, task)
if err != nil {
switch err := err.(type) {
case *serviceerror.Internal, *serviceerror.DataLoss:
e.nonRetryableErrorsDropTask(task, taskQueueName, err)
// drop the task as otherwise task would be stuck in a retry-loop
task.finish(nil, false)
case *serviceerror.NotFound: // mutable state not found, workflow not running or activity info not found
e.logger.Info("Activity task not found",
tag.WorkflowNamespaceID(task.event.Data.GetNamespaceId()),
tag.WorkflowID(task.event.Data.GetWorkflowId()),
tag.WorkflowRunID(task.event.Data.GetRunId()),
tag.WorkflowTaskQueueName(taskQueueName),
tag.TaskID(task.event.GetTaskId()),
tag.TaskVisibilityTimestamp(timestamp.TimeValue(task.event.Data.GetCreateTime())),
tag.WorkflowEventID(task.event.Data.GetScheduledEventId()),
tag.Error(err),
)
task.finish(nil, false)
case *serviceerrors.TaskAlreadyStarted:
e.logger.Debug("Duplicated activity task", tag.WorkflowTaskQueueName(taskQueueName), tag.TaskID(task.event.GetTaskId()))
task.finish(nil, false)
case *serviceerrors.ObsoleteDispatchBuildId:
// history should've scheduled another task on the right build ID. dropping this one.
e.logger.Info("dropping activity task due to invalid build ID",
tag.WorkflowTaskQueueName(taskQueueName),
tag.WorkflowNamespaceID(task.event.Data.GetNamespaceId()),
tag.WorkflowID(task.event.Data.GetWorkflowId()),
tag.WorkflowRunID(task.event.Data.GetRunId()),
tag.TaskID(task.event.GetTaskId()),
tag.TaskVisibilityTimestamp(timestamp.TimeValue(task.event.Data.GetCreateTime())),
tag.BuildId(requestClone.WorkerVersionCapabilities.GetBuildId()),
)
task.finish(nil, false)
case *serviceerrors.ObsoleteMatchingTask:
// History should've scheduled another task on the right task queue and deployment.
// Dropping this one.
e.logger.Info("dropping obsolete activity task",
tag.WorkflowTaskQueueName(taskQueueName),
tag.WorkflowNamespaceID(task.event.Data.GetNamespaceId()),
tag.WorkflowID(task.event.Data.GetWorkflowId()),
tag.WorkflowRunID(task.event.Data.GetRunId()),
tag.TaskID(task.event.GetTaskId()),
tag.WorkflowScheduledEventID(task.event.Data.GetScheduledEventId()),
tag.TaskVisibilityTimestamp(timestamp.TimeValue(task.event.Data.GetCreateTime())),
tag.VersioningBehavior(task.event.Data.VersionDirective.GetBehavior()),
//nolint:staticcheck // SA1019 deprecated WorkerVersionCapabilities will clean up later
tag.Deployment(worker_versioning.DeploymentNameFromCapabilities(requestClone.WorkerVersionCapabilities, requestClone.DeploymentOptions)),
//nolint:staticcheck // SA1019 deprecated WorkerVersionCapabilities will clean up later
tag.BuildId(worker_versioning.BuildIdFromCapabilities(requestClone.WorkerVersionCapabilities, requestClone.DeploymentOptions)),
tag.Error(err),
)
task.finish(nil, false)
case *serviceerrors.ActivityStartDuringTransition:
// History will schedule another task once transition ends. Dropping this one.
e.logger.Info("dropping activity task during transition",
tag.WorkflowTaskQueueName(taskQueueName),
tag.WorkflowNamespaceID(task.event.Data.GetNamespaceId()),
tag.WorkflowID(task.event.Data.GetWorkflowId()),
tag.WorkflowRunID(task.event.Data.GetRunId()),
tag.TaskID(task.event.GetTaskId()),
tag.WorkflowScheduledEventID(task.event.Data.GetScheduledEventId()),
tag.TaskVisibilityTimestamp(timestamp.TimeValue(task.event.Data.GetCreateTime())),
tag.VersioningBehavior(task.event.Data.VersionDirective.GetBehavior()),
//nolint:staticcheck // SA1019 deprecated WorkerVersionCapabilities will clean up later
tag.Deployment(worker_versioning.DeploymentNameFromCapabilities(requestClone.WorkerVersionCapabilities, requestClone.DeploymentOptions)),
//nolint:staticcheck // SA1019 deprecated WorkerVersionCapabilities will clean up later
tag.BuildId(worker_versioning.BuildIdFromCapabilities(requestClone.WorkerVersionCapabilities, requestClone.DeploymentOptions)),
)
task.finish(nil, false)
case *serviceerror.ResourceExhausted:
// If history returns one ResourceExhausted, it's likely to return more if we retry
// immediately. Instead, return the error to the client which will back off.
// BUSY_WORKFLOW is limited to one workflow and is okay to retry.
task.finish(err, false)
if err.Cause != enumspb.RESOURCE_EXHAUSTED_CAUSE_BUSY_WORKFLOW {
return nil, err
}
default:
task.finish(err, false)
if err.Error() == common.ErrNamespaceHandover.Error() {
// do not keep polling new tasks when namespace is in handover state
// as record start request will be rejected by history service
return nil, err
}
}
continue pollLoop
}
task.finish(nil, true)
return e.createPollActivityTaskQueueResponse(task, resp, opMetrics), nil
}
}
type queryResult struct {
workerResponse *matchingservice.RespondQueryTaskCompletedRequest
internalError error
}
// QueryWorkflow creates a WorkflowTask with query data, send it through sync match channel, wait for that WorkflowTask
// to be processed by worker, and then return the query result.
func (e *matchingEngineImpl) QueryWorkflow(
ctx context.Context,
queryRequest *matchingservice.QueryWorkflowRequest,
) (*matchingservice.QueryWorkflowResponse, error) {
partition, err := tqid.PartitionFromProto(queryRequest.TaskQueue, queryRequest.GetNamespaceId(), enumspb.TASK_QUEUE_TYPE_WORKFLOW)
if err != nil {
return nil, err
}
sticky := partition.Kind() == enumspb.TASK_QUEUE_KIND_STICKY
// do not load sticky task queue if it is not already loaded, which means it has no poller.
pm, _, err := e.getTaskQueuePartitionManager(ctx, partition, !sticky, loadCauseQuery)
if err != nil {
return nil, err
} else if sticky && !stickyWorkerAvailable(pm) {
return nil, serviceerrors.NewStickyWorkerUnavailable()
}
taskID := uuid.New()
resp, err := pm.DispatchQueryTask(ctx, taskID, queryRequest)
// if we get a response or error it means that query task was handled by forwarding to another matching host
// this remote host's result can be returned directly
if resp != nil || err != nil {
return resp, err
}
// if we get here it means that dispatch of query task has occurred locally
// must wait on result channel to get query result
queryResultCh := make(chan *queryResult, 1)
e.queryResults.Set(taskID, queryResultCh)
defer e.queryResults.Delete(taskID)
select {
case result := <-queryResultCh:
if result.internalError != nil {
return nil, result.internalError
}
workerResponse := result.workerResponse
switch workerResponse.GetCompletedRequest().GetCompletedType() {
case enumspb.QUERY_RESULT_TYPE_ANSWERED:
return &matchingservice.QueryWorkflowResponse{QueryResult: workerResponse.GetCompletedRequest().GetQueryResult()}, nil
case enumspb.QUERY_RESULT_TYPE_FAILED:
return nil, serviceerror.NewQueryFailedWithFailure(workerResponse.GetCompletedRequest().GetErrorMessage(), workerResponse.GetCompletedRequest().GetFailure())
default:
return nil, serviceerror.NewInternal("unknown query completed type")
}
case <-ctx.Done():
// task timed out. log (optionally) and return the timeout error
ns, err := e.namespaceRegistry.GetNamespaceByID(namespace.ID(partition.NamespaceId()))
if err != nil {
e.logger.Error("Failed to get the namespace by ID",
tag.WorkflowNamespaceID(partition.NamespaceId()),
tag.Error(err))
} else {
sampleRate := e.config.QueryWorkflowTaskTimeoutLogRate(ns.Name().String(), partition.TaskQueue().Name(), enumspb.TASK_QUEUE_TYPE_WORKFLOW)
if rand.Float64() < sampleRate {
e.logger.Info("Workflow Query Task timed out",
tag.WorkflowNamespaceID(ns.ID().String()),
tag.WorkflowNamespace(ns.Name().String()),
tag.WorkflowID(queryRequest.GetQueryRequest().GetExecution().GetWorkflowId()),
tag.WorkflowRunID(queryRequest.GetQueryRequest().GetExecution().GetRunId()),
tag.WorkflowTaskRequestId(taskID),
tag.WorkflowTaskQueueName(partition.TaskQueue().Name()))
}
}
return nil, ctx.Err()
}
}
func (e *matchingEngineImpl) RespondQueryTaskCompleted(
_ context.Context,
request *matchingservice.RespondQueryTaskCompletedRequest,
opMetrics metrics.Handler,
) error {
if err := e.deliverQueryResult(request.GetTaskId(), &queryResult{workerResponse: request}); err != nil {
metrics.RespondQueryTaskFailedPerTaskQueueCounter.With(opMetrics).Record(1)
return err
}
return nil
}
func (e *matchingEngineImpl) deliverQueryResult(taskID string, queryResult *queryResult) error {
queryResultCh, ok := e.queryResults.Pop(taskID)
if !ok {
return serviceerror.NewNotFound("query task not found, or already expired")
}
queryResultCh <- queryResult
return nil
}
func (e *matchingEngineImpl) CancelOutstandingPoll(
_ context.Context,
request *matchingservice.CancelOutstandingPollRequest,
) error {
cancel, ok := e.outstandingPollers.Pop(request.PollerId)
if ok {
cancel()
}
return nil
}
func (e *matchingEngineImpl) DescribeTaskQueue(
ctx context.Context,
request *matchingservice.DescribeTaskQueueRequest,
) (*matchingservice.DescribeTaskQueueResponse, error) {
req := request.GetDescRequest()
// This has been deprecated.
if req.ApiMode == enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED {
rootPartition, err := tqid.PartitionFromProto(req.GetTaskQueue(), request.GetNamespaceId(), req.GetTaskQueueType())
if err != nil {
return nil, err
}
tqConfig := newTaskQueueConfig(rootPartition.TaskQueue(), e.config, namespace.Name(req.Namespace))
if !rootPartition.IsRoot() || rootPartition.Kind() == enumspb.TASK_QUEUE_KIND_STICKY || rootPartition.TaskType() != enumspb.TASK_QUEUE_TYPE_WORKFLOW {
return nil, serviceerror.NewInvalidArgument("DescribeTaskQueue must be called on the root partition of workflow task queue if api mode is DESCRIBE_TASK_QUEUE_MODE_ENHANCED")
}
userData, err := e.getUserDataClone(ctx, rootPartition, loadCauseDescribe)
if err != nil {
return nil, err
}
if req.GetVersions() == nil {
defaultBuildId := getDefaultBuildId(userData.GetVersioningData().GetAssignmentRules())
req.Versions = &taskqueuepb.TaskQueueVersionSelection{BuildIds: []string{defaultBuildId}}
}
rootPM, _, err := e.getTaskQueuePartitionManager(ctx, rootPartition, true, loadCauseDescribe)
if err != nil {
return nil, err
}
// TODO bug fix: We cache the last response for each build ID. timeSinceLastFanOut is the last fan out time, that means some enteries in the cache can be more stale if
// user is calling this API back-to-back but with different version selection.
cacheKeyFunc := func(buildId string, taskQueueType enumspb.TaskQueueType) string {
return fmt.Sprintf("dtq_enhanced:%s.%s", buildId, taskQueueType.String())
}
missingItemsInCache := false
physicalTqInfos := make(map[string]map[enumspb.TaskQueueType]*taskqueuespb.PhysicalTaskQueueInfo)
//nolint:staticcheck // SA1019 deprecated
requestedBuildIds, err := e.getBuildIds(req.Versions)
if err != nil {
return nil, err
}
for buildId := range requestedBuildIds {
physicalTqInfos[buildId] = make(map[enumspb.TaskQueueType]*taskqueuespb.PhysicalTaskQueueInfo)
//nolint:staticcheck // SA1019 deprecated
for _, taskQueueType := range req.TaskQueueTypes {
cacheKey := cacheKeyFunc(buildId, taskQueueType)
cachedInfo := rootPM.GetCache(cacheKey) // any expired cache entry will return nil
if cachedInfo == nil {
missingItemsInCache = true
break // once we find a missing item, we can stop checking the cache
}
//revive:disable-next-line:unchecked-type-assertion
physicalTqInfos[buildId][taskQueueType] = cachedInfo.(*taskqueuespb.PhysicalTaskQueueInfo)
}
if missingItemsInCache {
break // stop checking other build IDs if we already found missing items
}
}
if missingItemsInCache {
// Fan out to partitions to get the needed info
var foundItems []struct {
buildId string
taskQueueType enumspb.TaskQueueType
}
numPartitions := max(tqConfig.NumWritePartitions(), tqConfig.NumReadPartitions())
for _, taskQueueType := range req.TaskQueueTypes {
for i := 0; i < numPartitions; i++ {
partitionResp, err := e.matchingRawClient.DescribeTaskQueuePartition(ctx, &matchingservice.DescribeTaskQueuePartitionRequest{
NamespaceId: request.GetNamespaceId(),
TaskQueuePartition: &taskqueuespb.TaskQueuePartition{
TaskQueue: req.TaskQueue.Name,
TaskQueueType: taskQueueType,
PartitionId: &taskqueuespb.TaskQueuePartition_NormalPartitionId{NormalPartitionId: int32(i)},
},
Versions: req.GetVersions(),
ReportStats: req.GetReportStats(),
ReportPollers: req.GetReportPollers(),
})
if err != nil {
return nil, err
}
for buildId, vii := range partitionResp.VersionsInfoInternal {
foundItems = append(foundItems, struct {
buildId string
taskQueueType enumspb.TaskQueueType
}{buildId, taskQueueType})
if _, ok := physicalTqInfos[buildId]; !ok {
physicalTqInfos[buildId] = make(map[enumspb.TaskQueueType]*taskqueuespb.PhysicalTaskQueueInfo)
}
if physInfo, ok := physicalTqInfos[buildId][taskQueueType]; !ok {
physicalTqInfos[buildId][taskQueueType] = vii.PhysicalTaskQueueInfo
} else {
var mergedStats *taskqueuepb.TaskQueueStats
if req.GetReportStats() {
totalStats := physicalTqInfos[buildId][taskQueueType].TaskQueueStats
partitionStats := vii.PhysicalTaskQueueInfo.TaskQueueStats
mergedStats = &taskqueuepb.TaskQueueStats{
ApproximateBacklogCount: totalStats.ApproximateBacklogCount + partitionStats.ApproximateBacklogCount,
ApproximateBacklogAge: oldestBacklogAge(totalStats.ApproximateBacklogAge, partitionStats.ApproximateBacklogAge),
TasksAddRate: totalStats.TasksAddRate + partitionStats.TasksAddRate,
TasksDispatchRate: totalStats.TasksDispatchRate + partitionStats.TasksDispatchRate,
}
}
physicalTqInfos[buildId][taskQueueType] = &taskqueuespb.PhysicalTaskQueueInfo{
Pollers: dedupPollers(append(physInfo.GetPollers(), vii.PhysicalTaskQueueInfo.GetPollers()...)),
TaskQueueStats: mergedStats,
}
}
}
}
}
// put the found items into cache
for _, item := range foundItems {
physicalTqInfo := physicalTqInfos[item.buildId][item.taskQueueType]
rootPM.PutCache(cacheKeyFunc(item.buildId, item.taskQueueType), physicalTqInfo)
}
}
// smush internal info into versions info
versionsInfo := make(map[string]*taskqueuepb.TaskQueueVersionInfo, 0)
for bid, typeMap := range physicalTqInfos {
typesInfo := make(map[int32]*taskqueuepb.TaskQueueTypeInfo, 0)
for taskQueueType, physicalInfo := range typeMap {
typesInfo[int32(taskQueueType)] = &taskqueuepb.TaskQueueTypeInfo{
Pollers: physicalInfo.Pollers,
Stats: physicalInfo.TaskQueueStats,
}
}
var reachability enumspb.BuildIdTaskReachability
if req.GetReportTaskReachability() {
reachability, err = getBuildIdTaskReachability(ctx,
newReachabilityCalculator(
userData.GetVersioningData(),
e.reachabilityCache,
request.GetNamespaceId(),
req.GetNamespace(),
rootPartition.TaskQueue().Family(),
e.config.ReachabilityBuildIdVisibilityGracePeriod(req.GetNamespace()),
tqConfig,
),
e.metricsHandler,
e.logger,
bid,
)
if err != nil {
return nil, err
}
}
versionsInfo[bid] = &taskqueuepb.TaskQueueVersionInfo{
TypesInfo: typesInfo,
TaskReachability: reachability,
}
}
return &matchingservice.DescribeTaskQueueResponse{
DescResponse: &workflowservice.DescribeTaskQueueResponse{
VersionsInfo: versionsInfo,
},
}, nil
}
partition, err := tqid.PartitionFromProto(req.TaskQueue, request.GetNamespaceId(), req.TaskQueueType)
if err != nil {
return nil, err
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, partition, true, loadCauseDescribe)
if err != nil {
return nil, err
}
//nolint:staticcheck // SA1019 deprecated
descrResp, err := pm.LegacyDescribeTaskQueue(req.GetIncludeTaskQueueStatus())
if err != nil {
return nil, err
}
if req.ReportStats {
if !pm.Partition().IsRoot() {
return nil, serviceerror.NewInvalidArgument("DescribeTaskQueue stats are only supported for the root partition")
}
var buildIds []string
if request.Version != nil {
// A particular version was requested. This is only available internally; not user-facing.
buildIds = []string{worker_versioning.WorkerDeploymentVersionToStringV31(request.Version)}
}
// TODO(stephan): cache each version separately to allow re-use of cached stats
cacheKey := "dtq_default:" + strings.Join(buildIds, ",")
if ts := pm.GetCache(cacheKey); ts != nil {
//revive:disable-next-line:unchecked-type-assertion
cachedResp := ts.(*workflowservice.DescribeTaskQueueResponse)
descrResp.DescResponse.Stats = cachedResp.Stats
descrResp.DescResponse.StatsByPriorityKey = cachedResp.StatsByPriorityKey
} else {
taskQueueStats := &taskqueuepb.TaskQueueStats{}
taskQueueStatsByPriority := make(map[int32]*taskqueuepb.TaskQueueStats)
// No version was requested, so we need to query all versions.
if len(buildIds) == 0 {
userData, _, err := pm.GetUserDataManager().GetUserData()
if err != nil {
return nil, err
}
typedUserData := userData.GetData().GetPerType()[int32(pm.Partition().TaskType())]
for _, v := range typedUserData.GetDeploymentData().GetVersions() {
if v.GetVersion() == nil || v.GetVersion().GetDeploymentName() == "" || v.GetVersion().GetBuildId() == "" {
continue
}
buildId := worker_versioning.WorkerDeploymentVersionToStringV32(v.GetVersion())
buildIds = append(buildIds, buildId)
}
}
// query each partition for stats
// TODO(stephanos): don't query root partition again
for i := 0; i < pm.PartitionCount(); i++ {
partitionResp, err := e.matchingRawClient.DescribeTaskQueuePartition(ctx,
&matchingservice.DescribeTaskQueuePartitionRequest{
NamespaceId: request.GetNamespaceId(),
TaskQueuePartition: &taskqueuespb.TaskQueuePartition{
TaskQueue: req.TaskQueue.Name,
TaskQueueType: req.TaskQueueType,
PartitionId: &taskqueuespb.TaskQueuePartition_NormalPartitionId{NormalPartitionId: int32(i)},
},
Versions: &taskqueuepb.TaskQueueVersionSelection{
BuildIds: buildIds,
Unversioned: true,
},
ReportStats: true,
})
if err != nil {
return nil, err
}
for _, vii := range partitionResp.VersionsInfoInternal {
partitionStats := vii.PhysicalTaskQueueInfo.TaskQueueStatsByPriorityKey
for pri, priorityStats := range partitionStats {
if _, ok := taskQueueStatsByPriority[pri]; !ok {
taskQueueStatsByPriority[pri] = &taskqueuepb.TaskQueueStats{}
}
mergeStats(taskQueueStats, priorityStats)
mergeStats(taskQueueStatsByPriority[pri], priorityStats)
}
}
}
pm.PutCache(cacheKey, &workflowservice.DescribeTaskQueueResponse{
Stats: taskQueueStats,
StatsByPriorityKey: taskQueueStatsByPriority,
})
descrResp.DescResponse.Stats = taskQueueStats
descrResp.DescResponse.StatsByPriorityKey = taskQueueStatsByPriority
}
}
if req.GetReportConfig() {
userData, _, err := pm.GetUserDataManager().GetUserData()
if err != nil {
return nil, err
}
descrResp.DescResponse.Config = userData.GetData().GetPerType()[int32(req.GetTaskQueueType())].GetConfig()
}
effectiveRPS, sourceForEffectiveRPS := pm.GetRateLimitManager().GetEffectiveRPSAndSource()
descrResp.DescResponse.EffectiveRateLimit = &workflowservice.DescribeTaskQueueResponse_EffectiveRateLimit{
RequestsPerSecond: float32(effectiveRPS),
RateLimitSource: sourceForEffectiveRPS,
}
return descrResp, nil
}
func (e *matchingEngineImpl) DescribeVersionedTaskQueues(
ctx context.Context,
request *matchingservice.DescribeVersionedTaskQueuesRequest,
) (*matchingservice.DescribeVersionedTaskQueuesResponse, error) {
partition, err := tqid.PartitionFromProto(request.TaskQueue, request.GetNamespaceId(), request.TaskQueueType)
if err != nil {
return nil, err
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, partition, true, loadCauseDescribe)
if err != nil {
return nil, err
}
cacheKey := fmt.Sprintf("dvtq:%s", worker_versioning.WorkerDeploymentVersionToStringV31(request.Version))
if cached := pm.GetCache(cacheKey); cached != nil {
//revive:disable-next-line:unchecked-type-assertion
return cached.(*matchingservice.DescribeVersionedTaskQueuesResponse), nil
}
resp := &matchingservice.DescribeVersionedTaskQueuesResponse{}
for _, tq := range request.VersionTaskQueues {
tqResp, err := e.matchingRawClient.DescribeTaskQueue(ctx,
&matchingservice.DescribeTaskQueueRequest{
NamespaceId: request.GetNamespaceId(),
DescRequest: &workflowservice.DescribeTaskQueueRequest{
TaskQueue: &taskqueuepb.TaskQueue{
Name: tq.Name,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
TaskQueueType: tq.Type,
ReportStats: true,
},
Version: request.Version,
})
if err != nil {
return nil, err
}
resp.VersionTaskQueues = append(resp.VersionTaskQueues,
&matchingservice.DescribeVersionedTaskQueuesResponse_VersionTaskQueue{
Name: tq.Name,
Type: tq.Type,
Stats: tqResp.DescResponse.Stats,
StatsByPriorityKey: tqResp.DescResponse.StatsByPriorityKey,
})
}
pm.PutCache(cacheKey, resp)
return resp, nil
}
func dedupPollers(pollerInfos []*taskqueuepb.PollerInfo) []*taskqueuepb.PollerInfo {
allKeys := make(map[string]bool)
var list []*taskqueuepb.PollerInfo
for _, item := range pollerInfos {
if _, value := allKeys[item.GetIdentity()]; !value {
allKeys[item.GetIdentity()] = true
list = append(list, item)
}
}
return list
}
func (e *matchingEngineImpl) DescribeTaskQueuePartition(
ctx context.Context,
request *matchingservice.DescribeTaskQueuePartitionRequest,
) (*matchingservice.DescribeTaskQueuePartitionResponse, error) {
if request.GetVersions() == nil {
return nil, serviceerror.NewInvalidArgument("versions must not be nil, to describe the default queue, pass the default build ID as a member of the BuildIds list")
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, tqid.PartitionFromPartitionProto(request.GetTaskQueuePartition(), request.GetNamespaceId()), true, loadCauseDescribe)
if err != nil {
return nil, err
}
buildIds, err := e.getBuildIds(request.GetVersions())
if err != nil {
return nil, err
}
return pm.Describe(ctx, buildIds, request.GetVersions().GetAllActive(), request.GetReportStats(), request.GetReportPollers(), request.GetReportInternalTaskQueueStatus())
}
func (e *matchingEngineImpl) getBuildIds(versions *taskqueuepb.TaskQueueVersionSelection) (map[string]bool, error) {
buildIds := make(map[string]bool)
if versions != nil {
for _, bid := range versions.GetBuildIds() {
buildIds[bid] = true
}
if versions.GetUnversioned() {
buildIds[""] = true
}
}
return buildIds, nil
}
func (e *matchingEngineImpl) ListTaskQueuePartitions(
_ context.Context,
request *matchingservice.ListTaskQueuePartitionsRequest,
) (*matchingservice.ListTaskQueuePartitionsResponse, error) {
activityTaskQueueInfo, err := e.listTaskQueuePartitions(request, enumspb.TASK_QUEUE_TYPE_ACTIVITY)
if err != nil {
return nil, err
}
workflowTaskQueueInfo, err := e.listTaskQueuePartitions(request, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
if err != nil {
return nil, err
}
resp := matchingservice.ListTaskQueuePartitionsResponse{
ActivityTaskQueuePartitions: activityTaskQueueInfo,
WorkflowTaskQueuePartitions: workflowTaskQueueInfo,
}
return &resp, nil
}
func (e *matchingEngineImpl) listTaskQueuePartitions(request *matchingservice.ListTaskQueuePartitionsRequest, taskQueueType enumspb.TaskQueueType) ([]*taskqueuepb.TaskQueuePartitionMetadata, error) {
partitions, err := e.getAllPartitionRpcNames(
namespace.Name(request.GetNamespace()),
request.TaskQueue,
taskQueueType,
)
if err != nil {
return nil, err
}
partitionHostInfo := make([]*taskqueuepb.TaskQueuePartitionMetadata, len(partitions))
for i, partition := range partitions {
host, err := e.getHostInfo(partition)
if err != nil {
return nil, err
}
partitionHostInfo[i] = &taskqueuepb.TaskQueuePartitionMetadata{
Key: partition,
OwnerHostName: host,
}
}
return partitionHostInfo, nil
}
func (e *matchingEngineImpl) UpdateWorkerVersioningRules(
ctx context.Context,
request *matchingservice.UpdateWorkerVersioningRulesRequest,
) (*matchingservice.UpdateWorkerVersioningRulesResponse, error) {
req := request.GetRequest()
ns, err := e.namespaceRegistry.GetNamespace(namespace.Name(req.GetNamespace()))
if err != nil {
return nil, err
}
if ns.ID().String() != request.GetNamespaceId() {
return nil, serviceerror.NewInternal("Namespace ID does not match Namespace in wrapped command")
}
if req.GetTaskQueue() != request.GetTaskQueue() {
return nil, serviceerror.NewInternal("Task Queue does not match Task Queue in wrapped command")
}
// We only expect to receive task queue family name (root partition) here.
taskQueueFamily, err := tqid.NewTaskQueueFamily(ns.ID().String(), req.GetTaskQueue())
if err != nil {
return nil, err
}
tqMgr, _, err := e.getTaskQueuePartitionManager(ctx, taskQueueFamily.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition(), true, loadCauseOtherWrite)
if err != nil {
return nil, err
}
// we don't set updateOptions.TaskQueueLimitPerBuildId, because the Versioning Rule limits will be checked separately
// we don't set updateOptions.KnownVersion, because we handle external API call ordering with conflictToken
updateOptions := UserDataUpdateOptions{Source: "UpdateWorkerVersioningRules"}
cT := req.GetConflictToken()
var getResp *matchingservice.GetWorkerVersioningRulesResponse
var maxUpstreamBuildIDs int
_, err = tqMgr.GetUserDataManager().UpdateUserData(ctx, updateOptions, func(data *persistencespb.TaskQueueUserData) (*persistencespb.TaskQueueUserData, bool, error) {
clk := data.GetClock()
if clk == nil {
clk = hlc.Zero(e.clusterMeta.GetClusterID())
}
prevCT, err := clk.Marshal()
if err != nil {
return nil, false, err
}
if !bytes.Equal(cT, prevCT) {
return nil, false, serviceerror.NewFailedPreconditionf(
"provided conflict token '%v' does not match existing one '%v'", cT, prevCT,
)
}
updatedClock := hlc.Next(clk, e.timeSource)
var versioningData *persistencespb.VersioningData
switch req.GetOperation().(type) {
case *workflowservice.UpdateWorkerVersioningRulesRequest_InsertAssignmentRule:
versioningData, err = InsertAssignmentRule(
updatedClock,
data.GetVersioningData(),
req.GetInsertAssignmentRule(),
e.config.AssignmentRuleLimitPerQueue(ns.Name().String()),
)
case *workflowservice.UpdateWorkerVersioningRulesRequest_ReplaceAssignmentRule:
versioningData, err = ReplaceAssignmentRule(
updatedClock,
data.GetVersioningData(),
req.GetReplaceAssignmentRule(),
)
case *workflowservice.UpdateWorkerVersioningRulesRequest_DeleteAssignmentRule:
versioningData, err = DeleteAssignmentRule(
updatedClock,
data.GetVersioningData(),
req.GetDeleteAssignmentRule(),
)
case *workflowservice.UpdateWorkerVersioningRulesRequest_AddCompatibleRedirectRule:
versioningData, err = AddCompatibleRedirectRule(
updatedClock,
data.GetVersioningData(),
req.GetAddCompatibleRedirectRule(),
e.config.RedirectRuleLimitPerQueue(ns.Name().String()),
e.config.RedirectRuleMaxUpstreamBuildIDsPerQueue(ns.Name().String()),
)
case *workflowservice.UpdateWorkerVersioningRulesRequest_ReplaceCompatibleRedirectRule:
versioningData, err = ReplaceCompatibleRedirectRule(
updatedClock,
data.GetVersioningData(),
req.GetReplaceCompatibleRedirectRule(),
e.config.RedirectRuleMaxUpstreamBuildIDsPerQueue(ns.Name().String()),
)
case *workflowservice.UpdateWorkerVersioningRulesRequest_DeleteCompatibleRedirectRule:
versioningData, err = DeleteCompatibleRedirectRule(
updatedClock,
data.GetVersioningData(),
req.GetDeleteCompatibleRedirectRule(),
)
case *workflowservice.UpdateWorkerVersioningRulesRequest_CommitBuildId_:
versioningData, err = CommitBuildID(
updatedClock,
data.GetVersioningData(),
req.GetCommitBuildId(),
tqMgr.HasPollerAfter(req.GetCommitBuildId().GetTargetBuildId(), time.Now().Add(-versioningPollerSeenWindow)),
e.config.AssignmentRuleLimitPerQueue(ns.Name().String()),
)
}
if err != nil {
// operation can't be completed due to failed validation. no action, do not replicate, report error
return nil, false, err
}
// Get versioning data formatted for response
getResp, err = GetTimestampedWorkerVersioningRules(versioningData, updatedClock)
if err != nil {
return nil, false, err
}
// Get max upstream build IDs (min is 0, because we count number of upstream nodes)
activeRedirectRules := getActiveRedirectRules(versioningData.GetRedirectRules())
for _, rule := range activeRedirectRules {
upstream := getUpstreamBuildIds(rule.GetRule().GetTargetBuildId(), activeRedirectRules)
if len(upstream)+1 > maxUpstreamBuildIDs {
maxUpstreamBuildIDs = len(upstream) + 1
}
}
// Clean up tombstones after all fallible tasks are complete, once we know we are committing and replicating the changes.
// We can replicate tombstone cleanup, because it's just based on DeletionTimestamp, so no need to only do it locally.
versioningData = CleanupRuleTombstones(versioningData, e.config.DeletedRuleRetentionTime(ns.Name().String()))
// Avoid mutation
ret := common.CloneProto(data)
ret.Clock = updatedClock
ret.VersioningData = versioningData
return ret, true, nil
})
if err != nil {
return nil, err
}
// log resulting rule counts
assignmentRules := getResp.GetResponse().GetAssignmentRules()
redirectRules := getResp.GetResponse().GetCompatibleRedirectRules()
e.logger.Info("UpdateWorkerVersioningRules completed",
tag.WorkerVersioningRedirectRuleCount(len(redirectRules)),
tag.WorkerVersioningAssignmentRuleCount(len(assignmentRules)),
tag.WorkerVersioningMaxUpstreamBuildIDs(maxUpstreamBuildIDs))
return &matchingservice.UpdateWorkerVersioningRulesResponse{Response: &workflowservice.UpdateWorkerVersioningRulesResponse{
AssignmentRules: assignmentRules,
CompatibleRedirectRules: redirectRules,
ConflictToken: getResp.GetResponse().GetConflictToken(),
}}, nil
}
func (e *matchingEngineImpl) GetWorkerVersioningRules(
ctx context.Context,
request *matchingservice.GetWorkerVersioningRulesRequest,
) (*matchingservice.GetWorkerVersioningRulesResponse, error) {
req := request.GetRequest()
ns, err := e.namespaceRegistry.GetNamespace(namespace.Name(req.GetNamespace()))
if err != nil {
return nil, err
}
if ns.ID().String() != request.GetNamespaceId() {
return nil, serviceerror.NewInternal("Namespace ID does not match Namespace in wrapped command")
}
if req.GetTaskQueue() != request.GetTaskQueue() {
return nil, serviceerror.NewInternal("Task Queue does not match Task Queue in wrapped command")
}
// We only expect to receive task queue family name (root partition) here.
taskQueueFamily, err := tqid.NewTaskQueueFamily(ns.ID().String(), req.GetTaskQueue())
if err != nil {
return nil, err
}
userData, err := e.getUserDataClone(ctx, taskQueueFamily.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition(), loadCauseOtherRead)
if err != nil {
return nil, err
}
clk := userData.GetClock()
if clk == nil {
clk = hlc.Zero(e.clusterMeta.GetClusterID())
}
return GetTimestampedWorkerVersioningRules(userData.GetVersioningData(), clk)
}
func (e *matchingEngineImpl) getUserDataClone(
ctx context.Context,
rootPartition tqid.Partition,
loadCause loadCause,
) (*persistencespb.TaskQueueUserData, error) {
rootPartitionMgr, _, err := e.getTaskQueuePartitionManager(ctx, rootPartition, true, loadCause)
if err != nil {
return nil, err
}
userData, _, err := rootPartitionMgr.GetUserDataManager().GetUserData()
if err != nil {
return nil, err
}
if userData == nil {
userData = &persistencespb.VersionedTaskQueueUserData{Data: &persistencespb.TaskQueueUserData{}}
} else {
userData = common.CloneProto(userData)
}
return userData.GetData(), nil
}
func (e *matchingEngineImpl) UpdateWorkerBuildIdCompatibility(
ctx context.Context,
req *matchingservice.UpdateWorkerBuildIdCompatibilityRequest,
) (*matchingservice.UpdateWorkerBuildIdCompatibilityResponse, error) {
namespaceID := namespace.ID(req.GetNamespaceId())
ns, err := e.namespaceRegistry.GetNamespaceByID(namespaceID)
if err != nil {
return nil, err
}
taskQueue, err := tqid.NewTaskQueueFamily(req.NamespaceId, req.GetTaskQueue())
if err != nil {
return nil, err
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, taskQueue.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition(), true, loadCauseOtherWrite)
if err != nil {
return nil, err
}
updateOptions := UserDataUpdateOptions{Source: "UpdateWorkerBuildIdCompatibility"}
operationCreatedTombstones := false
switch req.GetOperation().(type) {
case *matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest_:
// Only apply the limit when request is initiated by a user.
updateOptions.TaskQueueLimitPerBuildId = e.config.TaskQueueLimitPerBuildId(ns.Name().String())
case *matchingservice.UpdateWorkerBuildIdCompatibilityRequest_RemoveBuildIds_:
updateOptions.KnownVersion = req.GetRemoveBuildIds().GetKnownUserDataVersion()
}
_, err = pm.GetUserDataManager().UpdateUserData(ctx, updateOptions, func(data *persistencespb.TaskQueueUserData) (*persistencespb.TaskQueueUserData, bool, error) {
clk := data.GetClock()
if clk == nil {
tmp := hlc.Zero(e.clusterMeta.GetClusterID())
clk = tmp
}
updatedClock := hlc.Next(clk, e.timeSource)
var versioningData *persistencespb.VersioningData
switch req.GetOperation().(type) {
case *matchingservice.UpdateWorkerBuildIdCompatibilityRequest_ApplyPublicRequest_:
var err error
versioningData, err = UpdateVersionSets(
updatedClock,
data.GetVersioningData(),
req.GetApplyPublicRequest().GetRequest(),
e.config.VersionCompatibleSetLimitPerQueue(ns.Name().String()),
e.config.VersionBuildIdLimitPerQueue(ns.Name().String()),
)
if err != nil {
return nil, false, err
}
case *matchingservice.UpdateWorkerBuildIdCompatibilityRequest_RemoveBuildIds_:
versioningData = RemoveBuildIds(
updatedClock,
data.GetVersioningData(),
req.GetRemoveBuildIds().GetBuildIds(),
)
if ns.ReplicationPolicy() == namespace.ReplicationPolicyMultiCluster {
operationCreatedTombstones = true
} else {
// We don't need to keep the tombstones around if we're not replicating them.
versioningData = ClearTombstones(versioningData)
}
case *matchingservice.UpdateWorkerBuildIdCompatibilityRequest_PersistUnknownBuildId:
versioningData = PersistUnknownBuildId(
updatedClock,
data.GetVersioningData(),
req.GetPersistUnknownBuildId(),
)
default:
return nil, false, serviceerror.NewInvalidArgumentf("invalid operation: %v", req.GetOperation())
}
// Avoid mutation
ret := common.CloneProto(data)
ret.Clock = updatedClock
ret.VersioningData = versioningData
return ret, true, nil
})
if err != nil {
return nil, err
}
// Only clear tombstones after they have been replicated.
if operationCreatedTombstones {
opts := UserDataUpdateOptions{Source: "UpdateWorkerBuildIdCompatibility/clear-tombstones"}
_, err = pm.GetUserDataManager().UpdateUserData(ctx, opts, func(data *persistencespb.TaskQueueUserData) (*persistencespb.TaskQueueUserData, bool, error) {
updatedClock := hlc.Next(data.GetClock(), e.timeSource)
// Avoid mutation
ret := common.CloneProto(data)
ret.Clock = updatedClock
ret.VersioningData = ClearTombstones(data.VersioningData)
return ret, false, nil // Do not replicate the deletion of tombstones
})
if err != nil {
return nil, err
}
}
return &matchingservice.UpdateWorkerBuildIdCompatibilityResponse{}, nil
}
func (e *matchingEngineImpl) GetWorkerBuildIdCompatibility(
ctx context.Context,
req *matchingservice.GetWorkerBuildIdCompatibilityRequest,
) (*matchingservice.GetWorkerBuildIdCompatibilityResponse, error) {
taskQueueFamily, err := tqid.NewTaskQueueFamily(req.NamespaceId, req.Request.GetTaskQueue())
if err != nil {
return nil, err
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, taskQueueFamily.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition(), true, loadCauseOtherRead)
if err != nil {
if _, ok := err.(*serviceerror.NotFound); ok {
return &matchingservice.GetWorkerBuildIdCompatibilityResponse{}, nil
}
return nil, err
}
userData, _, err := pm.GetUserDataManager().GetUserData()
if err != nil {
return nil, err
}
return &matchingservice.GetWorkerBuildIdCompatibilityResponse{
Response: ToBuildIdOrderingResponse(userData.GetData().GetVersioningData(), int(req.GetRequest().GetMaxSets())),
}, nil
}
func (e *matchingEngineImpl) GetTaskQueueUserData(
ctx context.Context,
req *matchingservice.GetTaskQueueUserDataRequest,
) (*matchingservice.GetTaskQueueUserDataResponse, error) {
partition, err := tqid.PartitionFromProto(&taskqueuepb.TaskQueue{Name: req.GetTaskQueue()}, req.NamespaceId, req.TaskQueueType)
if err != nil {
return nil, err
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, partition, !req.OnlyIfLoaded, loadCauseUserData)
if err != nil {
return nil, err
} else if pm == nil {
return nil, serviceerror.NewFailedPrecondition("partition was not loaded")
}
if req.WaitNewData {
// mark alive so that it doesn't unload while a child partition is doing a long poll
pm.MarkAlive()
}
return pm.GetUserDataManager().HandleGetUserDataRequest(ctx, req)
}
func (e *matchingEngineImpl) SyncDeploymentUserData(
ctx context.Context,
req *matchingservice.SyncDeploymentUserDataRequest,
) (*matchingservice.SyncDeploymentUserDataResponse, error) {
taskQueueFamily, err := tqid.NewTaskQueueFamily(req.NamespaceId, req.GetTaskQueue())
if err != nil {
return nil, err
}
if req.Deployment == nil && req.GetOperation() == nil {
return nil, errMissingDeploymentVersion
}
tqMgr, _, err := e.getTaskQueuePartitionManager(ctx, taskQueueFamily.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition(), true, loadCauseOtherWrite)
if err != nil {
return nil, err
}
updateOptions := UserDataUpdateOptions{Source: "SyncDeploymentUserData"}
version, err := tqMgr.GetUserDataManager().UpdateUserData(ctx, updateOptions, func(data *persistencespb.TaskQueueUserData) (*persistencespb.TaskQueueUserData, bool, error) {
clk := data.GetClock()
if clk == nil {
clk = hlc.Zero(e.clusterMeta.GetClusterID())
}
now := hlc.Next(clk, e.timeSource)
// clone the whole thing so we can just mutate
data = common.CloneProto(data)
// fill in enough structure so that we can set/append the new deployment data
if data == nil {
data = &persistencespb.TaskQueueUserData{}
}
if data.PerType == nil {
data.PerType = make(map[int32]*persistencespb.TaskQueueTypeUserData)
}
if req.TaskQueueType != enumspb.TASK_QUEUE_TYPE_UNSPECIFIED {
req.TaskQueueTypes = append(req.TaskQueueTypes, req.TaskQueueType)
}
changed := false
for _, t := range req.TaskQueueTypes {
if data.PerType[int32(t)] == nil {
data.PerType[int32(t)] = &persistencespb.TaskQueueTypeUserData{}
}
if data.PerType[int32(t)].DeploymentData == nil {
data.PerType[int32(t)].DeploymentData = &persistencespb.DeploymentData{}
}
// set/append the new data
deploymentData := data.PerType[int32(t)].DeploymentData
if d := req.Deployment; d != nil {
// [cleanup-old-wv]
//nolint:staticcheck
if idx := worker_versioning.FindDeployment(deploymentData, req.Deployment); idx >= 0 {
deploymentData.Deployments[idx].Data = req.Data
} else {
deploymentData.Deployments = append(
deploymentData.Deployments, &persistencespb.DeploymentData_DeploymentDataItem{
Deployment: req.Deployment,
Data: req.Data,
})
}
changed = true
} else if vd := req.GetUpdateVersionData(); vd != nil {
if vd.GetVersion() == nil { // unversioned ramp
if deploymentData.GetUnversionedRampData().GetRoutingUpdateTime().AsTime().After(vd.GetRoutingUpdateTime().AsTime()) {
continue
}
changed = true
// only update if the timestamp is more recent
if vd.GetRampingSinceTime() == nil { // unset
deploymentData.UnversionedRampData = nil
} else { // set or update
deploymentData.UnversionedRampData = vd
}
} else if idx := worker_versioning.FindDeploymentVersion(deploymentData, vd.GetVersion()); idx >= 0 {
old := deploymentData.Versions[idx]
if old.GetRoutingUpdateTime().AsTime().After(vd.GetRoutingUpdateTime().AsTime()) {
continue
}
changed = true
// only update if the timestamp is more recent
deploymentData.Versions[idx] = vd
} else {
changed = true
deploymentData.Versions = append(deploymentData.Versions, vd)
}
} else if v := req.GetForgetVersion(); v != nil {
if idx := worker_versioning.FindDeploymentVersion(deploymentData, v); idx >= 0 {
changed = true
deploymentData.Versions = append(deploymentData.Versions[:idx], deploymentData.Versions[idx+1:]...)
}
}
}
if !changed {
return nil, false, errUserDataUnmodified
}
data.Clock = now
return data, true, nil
})
if err != nil {
return nil, err
}
return &matchingservice.SyncDeploymentUserDataResponse{Version: version}, nil
}
func (e *matchingEngineImpl) ApplyTaskQueueUserDataReplicationEvent(
ctx context.Context,
req *matchingservice.ApplyTaskQueueUserDataReplicationEventRequest,
) (*matchingservice.ApplyTaskQueueUserDataReplicationEventResponse, error) {
namespaceID := namespace.ID(req.GetNamespaceId())
ns, err := e.namespaceRegistry.GetNamespaceByID(namespaceID)
if err != nil {
return nil, err
}
taskQueueFamily, err := tqid.NewTaskQueueFamily(req.NamespaceId, req.GetTaskQueue())
if err != nil {
return nil, err
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, taskQueueFamily.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition(), true, loadCauseUserData)
if err != nil {
return nil, err
}
updateOptions := UserDataUpdateOptions{
// Avoid setting a limit to allow the replication event to always be applied
TaskQueueLimitPerBuildId: 0,
Source: "ApplyTaskQueueUserDataReplicationEvent",
}
_, err = pm.GetUserDataManager().UpdateUserData(ctx, updateOptions, func(current *persistencespb.TaskQueueUserData) (*persistencespb.TaskQueueUserData, bool, error) {
mergedUserData := common.CloneProto(current)
currentVersioningData := current.GetVersioningData()
newVersioningData := req.GetUserData().GetVersioningData()
_, buildIdsRemoved := GetBuildIdDeltas(currentVersioningData, newVersioningData)
var buildIdsToRevive []string
for _, buildId := range buildIdsRemoved {
// We accept that the user data is locked for updates while running these visibility queries.
// Nothing else is _supposed_ to update it on follower (standby) clusters.
exists, err := worker_versioning.WorkflowsExistForBuildId(ctx, e.visibilityManager, ns, req.TaskQueue, buildId)
if err != nil {
return nil, false, err
}
if exists {
buildIdsToRevive = append(buildIdsToRevive, buildId)
}
}
// merge v1 sets
mergedData := MergeVersioningData(currentVersioningData, newVersioningData)
// take last writer for V2 rules and V3 data
if req.GetUserData().GetClock() == nil || current.GetClock() != nil && hlc.Greater(current.GetClock(), req.GetUserData().GetClock()) {
if mergedData != nil {
// v2 rules
mergedData.AssignmentRules = currentVersioningData.GetAssignmentRules()
mergedData.RedirectRules = currentVersioningData.GetRedirectRules()
}
mergedUserData.PerType = current.GetPerType()
} else {
if mergedData != nil {
// v2 rules
mergedData.AssignmentRules = newVersioningData.GetAssignmentRules()
mergedData.RedirectRules = newVersioningData.GetRedirectRules()
}
mergedUserData.PerType = req.GetUserData().GetPerType()
}
for _, buildId := range buildIdsToRevive {
setIdx, buildIdIdx := worker_versioning.FindBuildId(mergedData, buildId)
if setIdx == -1 {
continue
}
set := mergedData.VersionSets[setIdx]
set.BuildIds[buildIdIdx] = e.reviveBuildId(ns, req.GetTaskQueue(), set.GetBuildIds()[buildIdIdx])
mergedUserData.Clock = hlc.Max(mergedUserData.Clock, set.BuildIds[buildIdIdx].StateUpdateTimestamp)
setDefault := set.BuildIds[len(set.BuildIds)-1]
if setDefault.State == persistencespb.STATE_DELETED {
// We merged an update which removed (at least) two build ids: the default for set x and another one for set
// x. We discovered we're still using the other one, so we revive it. now we also have to revive the default
// for set x, or it will be left with the wrong default.
set.BuildIds[len(set.BuildIds)-1] = e.reviveBuildId(ns, req.GetTaskQueue(), setDefault)
mergedUserData.Clock = hlc.Max(mergedUserData.Clock, setDefault.StateUpdateTimestamp)
}
}
if mergedData != nil {
// No need to keep the v1 tombstones around after replication.
mergedUserData.VersioningData = ClearTombstones(mergedData)
}
return mergedUserData, len(buildIdsToRevive) > 0, nil
})
return &matchingservice.ApplyTaskQueueUserDataReplicationEventResponse{}, err
}
func (e *matchingEngineImpl) GetBuildIdTaskQueueMapping(
ctx context.Context,
req *matchingservice.GetBuildIdTaskQueueMappingRequest,
) (*matchingservice.GetBuildIdTaskQueueMappingResponse, error) {
taskQueues, err := e.taskManager.GetTaskQueuesByBuildId(ctx, &persistence.GetTaskQueuesByBuildIdRequest{
NamespaceID: req.NamespaceId,
BuildID: req.BuildId,
})
if err != nil {
return nil, err
}
return &matchingservice.GetBuildIdTaskQueueMappingResponse{TaskQueues: taskQueues}, nil
}
// TODO Shivam - remove this in 123
func (e *matchingEngineImpl) ForceUnloadTaskQueue(
ctx context.Context,
req *matchingservice.ForceUnloadTaskQueueRequest,
) (*matchingservice.ForceUnloadTaskQueueResponse, error) {
p, err := tqid.NormalPartitionFromRpcName(req.GetTaskQueue(), req.GetNamespaceId(), req.GetTaskQueueType())
if err != nil {
return nil, err
}
wasLoaded := e.unloadTaskQueuePartitionByKey(p, nil, unloadCauseForce)
return &matchingservice.ForceUnloadTaskQueueResponse{WasLoaded: wasLoaded}, nil
}
func (e *matchingEngineImpl) ForceLoadTaskQueuePartition(
ctx context.Context,
req *matchingservice.ForceLoadTaskQueuePartitionRequest,
) (*matchingservice.ForceLoadTaskQueuePartitionResponse, error) {
partition := tqid.PartitionFromPartitionProto(req.GetTaskQueuePartition(), req.GetNamespaceId())
// Leverage getTaskQueuePartitionManager to check and then create the partition
_, wasUnloaded, err := e.getTaskQueuePartitionManager(ctx, partition, true, loadCauseForce)
if err != nil {
return nil, err
}
return &matchingservice.ForceLoadTaskQueuePartitionResponse{WasUnloaded: wasUnloaded}, nil
}
func (e *matchingEngineImpl) ForceUnloadTaskQueuePartition(
ctx context.Context,
req *matchingservice.ForceUnloadTaskQueuePartitionRequest,
) (*matchingservice.ForceUnloadTaskQueuePartitionResponse, error) {
partition := tqid.PartitionFromPartitionProto(req.GetTaskQueuePartition(), req.GetNamespaceId())
wasLoaded := e.unloadTaskQueuePartitionByKey(partition, nil, unloadCauseForce)
return &matchingservice.ForceUnloadTaskQueuePartitionResponse{WasLoaded: wasLoaded}, nil
}
func (e *matchingEngineImpl) UpdateTaskQueueUserData(ctx context.Context, request *matchingservice.UpdateTaskQueueUserDataRequest) (*matchingservice.UpdateTaskQueueUserDataResponse, error) {
namespaceId := namespace.ID(request.NamespaceId)
var applied, conflicting bool
persistenceErr, ctxErr := e.getUserDataBatcher(namespaceId).Add(ctx, &userDataUpdate{
taskQueue: request.GetTaskQueue(),
update: persistence.SingleTaskQueueUserDataUpdate{
UserData: request.UserData,
BuildIdsAdded: request.BuildIdsAdded,
BuildIdsRemoved: request.BuildIdsRemoved,
Applied: &applied,
Conflicting: &conflicting,
},
})
if ctxErr != nil {
// Return context errors as-is.
return nil, ctxErr
}
// If applied is true, this one succeeded even though others in the batch failed.
if persistenceErr != nil && !applied {
if persistence.IsConflictErr(persistenceErr) {
if conflicting {
// This specific update was the conflicting one. Use InvalidArgument so the
// caller does not retry.
return nil, serviceerror.NewInvalidArgument(persistenceErr.Error())
}
// This update may or may not be conflicting. Use Unavailable to allow retries.
return nil, serviceerror.NewUnavailable(persistenceErr.Error())
}
// Other errors from persistence get returned as-is.
return nil, persistenceErr
}
return &matchingservice.UpdateTaskQueueUserDataResponse{}, nil
}
func (e *matchingEngineImpl) ReplicateTaskQueueUserData(ctx context.Context, request *matchingservice.ReplicateTaskQueueUserDataRequest) (*matchingservice.ReplicateTaskQueueUserDataResponse, error) {
if e.namespaceReplicationQueue == nil {
return &matchingservice.ReplicateTaskQueueUserDataResponse{}, nil
}
e.replicationLock.Lock()
defer e.replicationLock.Unlock()
err := e.namespaceReplicationQueue.Publish(ctx, &replicationspb.ReplicationTask{
TaskType: enumsspb.REPLICATION_TASK_TYPE_TASK_QUEUE_USER_DATA,
Attributes: &replicationspb.ReplicationTask_TaskQueueUserDataAttributes{
TaskQueueUserDataAttributes: &replicationspb.TaskQueueUserDataAttributes{
NamespaceId: request.GetNamespaceId(),
TaskQueueName: request.GetTaskQueue(),
UserData: request.GetUserData(),
},
},
})
return &matchingservice.ReplicateTaskQueueUserDataResponse{}, err
}
func (e *matchingEngineImpl) CheckTaskQueueUserDataPropagation(ctx context.Context, req *matchingservice.CheckTaskQueueUserDataPropagationRequest) (*matchingservice.CheckTaskQueueUserDataPropagationResponse, error) {
rootPartition, err := tqid.NormalPartitionFromRpcName(req.TaskQueue, req.NamespaceId, enumspb.TASK_QUEUE_TYPE_WORKFLOW)
if err != nil {
return nil, err
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, rootPartition, true, loadCauseOtherRead)
if err != nil {
return nil, err
}
nsName := pm.Namespace().Name().String()
tqName := rootPartition.TaskQueue().Name()
wfPartitions := max(
e.config.NumTaskqueueReadPartitions(nsName, tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW),
e.config.NumTaskqueueWritePartitions(nsName, tqName, enumspb.TASK_QUEUE_TYPE_WORKFLOW),
)
actPartitions := max(
e.config.NumTaskqueueReadPartitions(nsName, tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY),
e.config.NumTaskqueueWritePartitions(nsName, tqName, enumspb.TASK_QUEUE_TYPE_ACTIVITY),
)
err = pm.GetUserDataManager().CheckTaskQueueUserDataPropagation(ctx, req.Version, wfPartitions, actPartitions)
if err != nil {
return nil, err
}
return &matchingservice.CheckTaskQueueUserDataPropagationResponse{}, nil
}
// nexusResult is container for a response or error.
// Only one field may be set at a time.
type nexusResult struct {
successfulWorkerResponse *matchingservice.RespondNexusTaskCompletedRequest
failedWorkerResponse *matchingservice.RespondNexusTaskFailedRequest
internalError error
}
func (e *matchingEngineImpl) DispatchNexusTask(ctx context.Context, request *matchingservice.DispatchNexusTaskRequest) (*matchingservice.DispatchNexusTaskResponse, error) {
partition, err := tqid.PartitionFromProto(request.GetTaskQueue(), request.GetNamespaceId(), enumspb.TASK_QUEUE_TYPE_NEXUS)
if err != nil {
return nil, err
}
pm, _, err := e.getTaskQueuePartitionManager(ctx, partition, true, loadCauseNexusTask)
if err != nil {
return nil, err
}
taskID := uuid.New()
resp, err := pm.DispatchNexusTask(ctx, taskID, request)
// if we get a response or error it means that the Nexus task was handled by forwarding to another matching host
// this remote host's result can be returned directly
if resp != nil || err != nil {
return resp, err
}
// if we get here it means that dispatch of query task has occurred locally
// must wait on result channel to get query result
resultCh := make(chan *nexusResult, 1)
e.nexusResults.Set(taskID, resultCh)
defer e.nexusResults.Delete(taskID)
select {
case result := <-resultCh:
if result.internalError != nil {
return nil, result.internalError
}
if result.failedWorkerResponse != nil {
return &matchingservice.DispatchNexusTaskResponse{Outcome: &matchingservice.DispatchNexusTaskResponse_HandlerError{
HandlerError: result.failedWorkerResponse.GetRequest().GetError(),
}}, nil
}
return &matchingservice.DispatchNexusTaskResponse{Outcome: &matchingservice.DispatchNexusTaskResponse_Response{
Response: result.successfulWorkerResponse.GetRequest().GetResponse(),
}}, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (e *matchingEngineImpl) PollNexusTaskQueue(
ctx context.Context,
req *matchingservice.PollNexusTaskQueueRequest,
opMetrics metrics.Handler,
) (*matchingservice.PollNexusTaskQueueResponse, error) {
namespaceID := namespace.ID(req.GetNamespaceId())
pollerID := req.GetPollerId()
request := req.Request
taskQueueName := request.TaskQueue.GetName()
e.logger.Debug("Received PollNexusTaskQueue for taskQueue", tag.Name(taskQueueName))
pollLoop:
for {
err := common.IsValidContext(ctx)
if err != nil {
return nil, err
}
// Add frontend generated pollerID to context so taskqueueMgr can support cancellation of
// long-poll when frontend calls CancelOutstandingPoll API
pollerCtx := context.WithValue(ctx, pollerIDKey, pollerID)
pollerCtx = context.WithValue(pollerCtx, identityKey, request.GetIdentity())
partition, err := tqid.PartitionFromProto(request.TaskQueue, req.NamespaceId, enumspb.TASK_QUEUE_TYPE_NEXUS)
if err != nil {
return nil, err
}
pollMetadata := &pollMetadata{
workerVersionCapabilities: request.WorkerVersionCapabilities,
deploymentOptions: request.DeploymentOptions,
forwardedFrom: req.GetForwardedSource(),
}
task, _, err := e.pollTask(pollerCtx, partition, pollMetadata)
if err != nil {
if errors.Is(err, errNoTasks) {
return &matchingservice.PollNexusTaskQueueResponse{}, nil
}
return nil, err
}
if task.isStarted() {
// tasks received from remote are already started. So, simply forward the response
return task.pollNexusTaskQueueResponse(), nil
}
task.finish(err, true)
if err != nil {
continue pollLoop
}
taskToken := &tokenspb.NexusTask{
NamespaceId: string(namespaceID),
TaskQueue: taskQueueName,
TaskId: task.nexus.taskID,
}
serializedToken, _ := e.tokenSerializer.SerializeNexusTaskToken(taskToken)
nexusReq := task.nexus.request.GetRequest()
nexusReq.Header[nexus.HeaderRequestTimeout] = time.Until(task.nexus.deadline).String()
// Java SDK currently expects the header in this form. We should be able to remove this duplication sometime mid 2025.
nexusReq.Header["Request-Timeout"] = time.Until(task.nexus.deadline).String()
if !task.nexus.operationDeadline.IsZero() {
nexusReq.Header[nexus.HeaderOperationTimeout] = commonnexus.FormatDuration(time.Until(task.nexus.operationDeadline))
}
return &matchingservice.PollNexusTaskQueueResponse{
Response: &workflowservice.PollNexusTaskQueueResponse{
TaskToken: serializedToken,
Request: nexusReq,
PollerScalingDecision: task.pollerScalingDecision,
},
}, nil
}
}
func (e *matchingEngineImpl) RespondNexusTaskCompleted(ctx context.Context, request *matchingservice.RespondNexusTaskCompletedRequest, opMetrics metrics.Handler) (*matchingservice.RespondNexusTaskCompletedResponse, error) {
resultCh, ok := e.nexusResults.Pop(request.GetTaskId())
if !ok {
opMetrics.Counter(metrics.RespondNexusTaskFailedPerTaskQueueCounter.Name()).Record(1)
return nil, serviceerror.NewNotFound("Nexus task not found or already expired")
}
resultCh <- &nexusResult{
successfulWorkerResponse: request,
internalError: nil,
}
return &matchingservice.RespondNexusTaskCompletedResponse{}, nil
}
func (e *matchingEngineImpl) RespondNexusTaskFailed(ctx context.Context, request *matchingservice.RespondNexusTaskFailedRequest, opMetrics metrics.Handler) (*matchingservice.RespondNexusTaskFailedResponse, error) {
resultCh, ok := e.nexusResults.Pop(request.GetTaskId())
if !ok {
opMetrics.Counter(metrics.RespondNexusTaskFailedPerTaskQueueCounter.Name()).Record(1)
return nil, serviceerror.NewNotFound("Nexus task not found or already expired")
}
resultCh <- &nexusResult{
failedWorkerResponse: request,
internalError: nil,
}
return &matchingservice.RespondNexusTaskFailedResponse{}, nil
}
func (e *matchingEngineImpl) CreateNexusEndpoint(ctx context.Context, request *matchingservice.CreateNexusEndpointRequest) (*matchingservice.CreateNexusEndpointResponse, error) {
// Write API, let persistence verify table ownership.
res, err := e.nexusEndpointClient.CreateNexusEndpoint(ctx, &internalCreateNexusEndpointRequest{
spec: request.GetSpec(),
clusterID: e.clusterMeta.GetClusterID(),
timeSource: e.timeSource,
})
if err != nil {
e.logger.Error("Failed to create Nexus endpoint", tag.Error(err), tag.Endpoint(request.GetSpec().GetName()))
} else {
e.logger.Info("Created Nexus endpoint", tag.Endpoint(request.GetSpec().GetName()))
}
return res, err
}
func (e *matchingEngineImpl) UpdateNexusEndpoint(ctx context.Context, request *matchingservice.UpdateNexusEndpointRequest) (*matchingservice.UpdateNexusEndpointResponse, error) {
// Write API, let persistence verify table ownership.
res, err := e.nexusEndpointClient.UpdateNexusEndpoint(ctx, &internalUpdateNexusEndpointRequest{
endpointID: request.GetId(),
version: request.GetVersion(),
spec: request.GetSpec(),
clusterID: e.clusterMeta.GetClusterID(),
timeSource: e.timeSource,
})
if err != nil {
e.logger.Error("Failed to update Nexus endpoint", tag.Error(err), tag.Endpoint(request.GetSpec().GetName()))
} else {
e.logger.Info("Updated Nexus endpoint", tag.Endpoint(request.GetSpec().GetName()))
}
return res, err
}
func (e *matchingEngineImpl) DeleteNexusEndpoint(ctx context.Context, request *matchingservice.DeleteNexusEndpointRequest) (*matchingservice.DeleteNexusEndpointResponse, error) {
// Write API, let persistence verify table ownership.
res, err := e.nexusEndpointClient.DeleteNexusEndpoint(ctx, request)
if err != nil {
e.logger.Error("Failed to delete Nexus endpoint", tag.Error(err), tag.Endpoint(request.GetId()))
} else {
e.logger.Info("Deleted Nexus endpoint", tag.Endpoint(request.GetId()))
}
return res, err
}
func (e *matchingEngineImpl) ListNexusEndpoints(ctx context.Context, request *matchingservice.ListNexusEndpointsRequest) (*matchingservice.ListNexusEndpointsResponse, error) {
lastKnownVersion := request.LastKnownTableVersion
// Read API, verify table ownership via membership.
isOwner, ownershipLostCh, err := e.checkNexusEndpointsOwnership()
if err != nil {
e.logger.Error("Failed to check Nexus endpoints ownership", tag.Error(err))
return nil, serviceerror.NewAbortedf("cannot verify ownership of Nexus endpoints table: %v", err)
}
if !isOwner {
e.logger.Error("Matching node doesn't think it's the Nexus endpoints table owner", tag.Error(err))
return nil, serviceerror.NewAborted("matching node doesn't think it's the Nexus endpoints table owner")
}
if request.Wait {
if request.NextPageToken != nil {
return nil, serviceerror.NewInvalidArgument("request Wait=true and NextPageToken!=nil on ListNexusEndpoints request. waiting is only allowed on first page")
}
// if waiting, send request with unknown table version so we get the newest view of the table
request.LastKnownTableVersion = 0
var cancel context.CancelFunc
ctx, cancel = contextutil.WithDeadlineBuffer(ctx, e.config.ListNexusEndpointsLongPollTimeout(), returnEmptyTaskTimeBudget)
defer cancel()
}
for {
resp, tableVersionChanged, err := e.nexusEndpointClient.ListNexusEndpoints(ctx, request)
if err != nil {
return resp, err
}
if request.Wait && lastKnownVersion == resp.TableVersion {
// long-poll: wait for data to change/appear
select {
case <-ownershipLostCh:
return nil, serviceerror.NewAborted("Nexus endpoints table ownership lost")
case <-ctx.Done():
return resp, nil
case <-tableVersionChanged:
continue
}
}
return resp, err
}
}
func (e *matchingEngineImpl) checkNexusEndpointsOwnership() (bool, <-chan struct{}, error) {
// Get the channel before checking the condition to prevent the channel from being closed while we're running this
// check.
ch := e.nexusEndpointsOwnershipLostCh
self := e.hostInfoProvider.HostInfo().Identity()
owner, err := e.serviceResolver.Lookup(nexusEndpointsTablePartitionRoutingKey)
if err != nil {
return false, nil, fmt.Errorf("cannot resolve Nexus endpoints partition owner: %w", err)
}
return owner.Identity() == self, ch, nil
}
func (e *matchingEngineImpl) notifyNexusEndpointsOwnershipChange() {
// We don't care about the channel returned here. This method is ensured to only be called from the single
// watchMembership method and is the only way the channel may be replaced.
isOwner, _, err := e.checkNexusEndpointsOwnership()
if err != nil {
e.logger.Error("Failed to check Nexus endpoints ownership", tag.Error(err))
return
}
if !isOwner {
close(e.nexusEndpointsOwnershipLostCh)
e.nexusEndpointsOwnershipLostCh = make(chan struct{})
}
e.nexusEndpointClient.notifyOwnershipChanged(isOwner)
}
func (e *matchingEngineImpl) getUserDataBatcher(namespaceId namespace.ID) *stream_batcher.Batcher[*userDataUpdate, error] {
// Note that values are never removed from this map. The batcher's goroutine will exit
// after the idle time, though, which gets most of the desired resource savings.
if batcher, ok := e.userDataUpdateBatchers.Get(namespaceId); ok {
return batcher
}
fn := func(batch []*userDataUpdate) error {
return e.applyUserDataUpdateBatch(namespaceId, batch)
}
newBatcher := stream_batcher.NewBatcher[*userDataUpdate, error](fn, userDataBatcherOptions, e.timeSource)
batcher, _ := e.userDataUpdateBatchers.GetOrSet(namespaceId, newBatcher)
return batcher
}
func (e *matchingEngineImpl) applyUserDataUpdateBatch(namespaceId namespace.ID, batch []*userDataUpdate) error {
ctx, cancel := context.WithTimeout(context.Background(), ioTimeout)
// TODO: should use namespace name here
ctx = headers.SetCallerInfo(ctx, headers.NewBackgroundHighCallerInfo(namespaceId.String()))
defer cancel()
// convert to map
updatesMap := make(map[string]*persistence.SingleTaskQueueUserDataUpdate)
for _, update := range batch {
updatesMap[update.taskQueue] = &update.update
}
// now apply the batch of updates
return e.taskManager.UpdateTaskQueueUserData(ctx, &persistence.UpdateTaskQueueUserDataRequest{
NamespaceID: namespaceId.String(),
Updates: updatesMap,
})
}
func (e *matchingEngineImpl) getHostInfo(partitionKey string) (string, error) {
host, err := e.serviceResolver.Lookup(partitionKey)
if err != nil {
return "", err
}
return host.GetAddress(), nil
}
func (e *matchingEngineImpl) getAllPartitionRpcNames(
ns namespace.Name,
taskQueue *taskqueuepb.TaskQueue,
taskQueueType enumspb.TaskQueueType,
) ([]string, error) {
var partitionKeys []string
namespaceID, err := e.namespaceRegistry.GetNamespaceID(ns)
if err != nil {
return partitionKeys, err
}
taskQueueFamily, err := tqid.NewTaskQueueFamily(namespaceID.String(), taskQueue.GetName())
if err != nil {
return partitionKeys, err
}
n := e.config.NumTaskqueueWritePartitions(ns.String(), taskQueueFamily.Name(), taskQueueType)
for i := 0; i < n; i++ {
partitionKeys = append(partitionKeys, taskQueueFamily.TaskQueue(taskQueueType).NormalPartition(i).RpcName())
}
return partitionKeys, nil
}
func (e *matchingEngineImpl) pollTask(
ctx context.Context,
partition tqid.Partition,
pollMetadata *pollMetadata,
) (*internalTask, bool, error) {
pm, _, err := e.getTaskQueuePartitionManager(ctx, partition, true, loadCausePoll)
if err != nil {
return nil, false, err
}
pollMetadata.localPollStartTime = e.timeSource.Now()
// We need to set a shorter timeout than the original ctx; otherwise, by the time ctx deadline is
// reached, instead of emptyTask, context timeout error is returned to the frontend by the rpc stack,
// which counts against our SLO. By shortening the timeout by a very small amount, the emptyTask can be
// returned to the handler before a context timeout error is generated.
ctx, cancel := contextutil.WithDeadlineBuffer(ctx, pm.LongPollExpirationInterval(), returnEmptyTaskTimeBudget)
defer cancel()
if pollerID, ok := ctx.Value(pollerIDKey).(string); ok && pollerID != "" {
e.outstandingPollers.Set(pollerID, cancel)
defer e.outstandingPollers.Delete(pollerID)
}
return pm.PollTask(ctx, pollMetadata)
}
// Unloads the given task queue partition. If it has already been unloaded (i.e. it's not present in the loaded
// partitions map), then does nothing.
// partitions map), unloadPM.Stop(...) is still called.
func (e *matchingEngineImpl) unloadTaskQueuePartition(unloadPM taskQueuePartitionManager, unloadCause unloadCause) {
e.unloadTaskQueuePartitionByKey(unloadPM.Partition(), unloadPM, unloadCause)
}
// Unloads a task queue partition by id. If unloadPM is given and the loaded partition for queueID does not match
// unloadPM, then nothing is unloaded from matching engine (but unloadPM will be stopped).
// Returns true if it unloaded a partition and false if not.
func (e *matchingEngineImpl) unloadTaskQueuePartitionByKey(
partition tqid.Partition,
unloadPM taskQueuePartitionManager,
unloadCause unloadCause,
) bool {
key := partition.Key()
e.partitionsLock.Lock()
foundTQM, ok := e.partitions[key]
if !ok || (unloadPM != nil && foundTQM != unloadPM) {
e.partitionsLock.Unlock()
return false
}
delete(e.partitions, key)
e.partitionsLock.Unlock()
foundTQM.Stop(unloadCause)
return true
}
// Responsible for emitting and updating loaded_physical_task_queue_count metric
func (e *matchingEngineImpl) updatePhysicalTaskQueueGauge(
ns *namespace.Namespace,
partition tqid.Partition,
version PhysicalTaskQueueVersion,
delta int,
) {
// calculating versioned to be one of: “unversioned” or "buildId” or “versionSet”
versioned := "unversioned"
if dep := version.Deployment(); dep != nil {
versioned = "deployment"
} else if buildID := version.BuildId(); buildID != "" {
versioned = "buildId"
} else if versionSet := version.VersionSet(); versionSet != "" {
versioned = "versionSet"
}
physicalTaskQueueParameters := taskQueueCounterKey{
namespaceID: partition.NamespaceId(),
taskType: partition.TaskType(),
partitionType: partition.Kind(),
versioned: versioned,
}
e.gaugeMetrics.lock.Lock()
e.gaugeMetrics.loadedPhysicalTaskQueueCount[physicalTaskQueueParameters] += delta
loadedPhysicalTaskQueueCounter := e.gaugeMetrics.loadedPhysicalTaskQueueCount[physicalTaskQueueParameters]
e.gaugeMetrics.lock.Unlock()
metrics.LoadedPhysicalTaskQueueGauge.With(
metrics.GetPerTaskQueuePartitionTypeScope(
e.metricsHandler,
ns.Name().String(),
partition,
// TODO: Track counters per TQ name so we can honor pm.config.BreakdownMetricsByTaskQueue(),
false,
)).Record(
float64(loadedPhysicalTaskQueueCounter),
metrics.VersionedTag(versioned),
)
}
// Responsible for emitting and updating loaded_task_queue_family_count, loaded_task_queue_count and
// loaded_task_queue_partition_count metrics
func (e *matchingEngineImpl) updateTaskQueuePartitionGauge(
ns *namespace.Namespace,
partition tqid.Partition,
delta int,
) {
// each metric shall be accessed based on the mentioned parameters
taskQueueFamilyParameters := taskQueueCounterKey{
namespaceID: partition.NamespaceId(),
}
taskQueueParameters := taskQueueCounterKey{
namespaceID: partition.NamespaceId(),
taskType: partition.TaskType(),
}
taskQueuePartitionParameters := taskQueueCounterKey{
namespaceID: partition.NamespaceId(),
taskType: partition.TaskType(),
partitionType: partition.Kind(),
}
rootPartition := partition.IsRoot()
e.gaugeMetrics.lock.Lock()
loadedTaskQueueFamilyCounter, loadedTaskQueueCounter, loadedTaskQueuePartitionCounter :=
e.gaugeMetrics.loadedTaskQueueFamilyCount[taskQueueFamilyParameters], e.gaugeMetrics.loadedTaskQueueCount[taskQueueParameters],
e.gaugeMetrics.loadedTaskQueuePartitionCount[taskQueuePartitionParameters]
loadedTaskQueuePartitionCounter += delta
e.gaugeMetrics.loadedTaskQueuePartitionCount[taskQueuePartitionParameters] = loadedTaskQueuePartitionCounter
if rootPartition {
loadedTaskQueueCounter += delta
e.gaugeMetrics.loadedTaskQueueCount[taskQueueParameters] = loadedTaskQueueCounter
if partition.TaskType() == enumspb.TASK_QUEUE_TYPE_WORKFLOW {
loadedTaskQueueFamilyCounter += delta
e.gaugeMetrics.loadedTaskQueueFamilyCount[taskQueueFamilyParameters] = loadedTaskQueueFamilyCounter
}
}
e.gaugeMetrics.lock.Unlock()
nsName := ns.Name().String()
e.metricsHandler.Gauge(metrics.LoadedTaskQueueFamilyGauge.Name()).Record(
float64(loadedTaskQueueFamilyCounter),
metrics.NamespaceTag(nsName),
)
metrics.LoadedTaskQueueGauge.With(e.metricsHandler).Record(
float64(loadedTaskQueueCounter),
metrics.NamespaceTag(nsName),
metrics.TaskQueueTypeTag(taskQueueParameters.taskType),
)
taggedHandler := metrics.GetPerTaskQueuePartitionTypeScope(
e.metricsHandler,
nsName,
partition,
// TODO: Track counters per TQ name so we can honor pm.config.BreakdownMetricsByTaskQueue(),
false,
)
metrics.LoadedTaskQueuePartitionGauge.With(taggedHandler).Record(float64(loadedTaskQueuePartitionCounter))
}
// Populate the workflow task response based on context and scheduled/started events.
func (e *matchingEngineImpl) createPollWorkflowTaskQueueResponse(
task *internalTask,
recordStartResp *historyservice.RecordWorkflowTaskStartedResponse,
metricsHandler metrics.Handler,
) *matchingservice.PollWorkflowTaskQueueResponse {
var serializedToken []byte
if task.isQuery() {
// for a query task
queryRequest := task.query.request
queryTaskToken := &tokenspb.QueryTask{
NamespaceId: queryRequest.GetNamespaceId(),
TaskQueue: queryRequest.TaskQueue.Name,
TaskId: task.query.taskID,
}
serializedToken, _ = e.tokenSerializer.SerializeQueryTaskToken(queryTaskToken)
} else {
taskToken := tasktoken.NewWorkflowTaskToken(
task.event.Data.GetNamespaceId(),
task.event.Data.GetWorkflowId(),
task.event.Data.GetRunId(),
recordStartResp.GetScheduledEventId(),
recordStartResp.GetStartedEventId(),
recordStartResp.GetStartedTime(),
recordStartResp.GetAttempt(),
recordStartResp.GetClock(),
recordStartResp.GetVersion(),
)
serializedToken, _ = e.tokenSerializer.Serialize(taskToken)
if task.responseC == nil {
ct := timestamp.TimeValue(task.event.Data.CreateTime)
metrics.AsyncMatchLatencyPerTaskQueue.With(metricsHandler).Record(time.Since(ct))
}
}
response := common.CreateMatchingPollWorkflowTaskQueueResponse(
recordStartResp,
task.workflowExecution(),
serializedToken)
if task.query != nil {
response.Query = task.query.request.QueryRequest.Query
}
if task.backlogCountHint != nil {
response.BacklogCountHint = task.backlogCountHint()
}
response.PollerScalingDecision = task.pollerScalingDecision
return response
}
// Populate the activity task response based on context and scheduled/started events.
func (e *matchingEngineImpl) createPollActivityTaskQueueResponse(
task *internalTask,
historyResponse *historyservice.RecordActivityTaskStartedResponse,
metricsHandler metrics.Handler,
) *matchingservice.PollActivityTaskQueueResponse {
scheduledEvent := historyResponse.ScheduledEvent
if scheduledEvent.GetActivityTaskScheduledEventAttributes() == nil {
panic("GetActivityTaskScheduledEventAttributes is not set")
}
attributes := scheduledEvent.GetActivityTaskScheduledEventAttributes()
if attributes.ActivityId == "" {
panic("ActivityTaskScheduledEventAttributes.ActivityID is not set")
}
if task.responseC == nil {
ct := timestamp.TimeValue(task.event.Data.CreateTime)
metrics.AsyncMatchLatencyPerTaskQueue.With(metricsHandler).Record(time.Since(ct))
}
taskToken := tasktoken.NewActivityTaskToken(
task.event.Data.GetNamespaceId(),
task.event.Data.GetWorkflowId(),
task.event.Data.GetRunId(),
task.event.Data.GetScheduledEventId(),
attributes.GetActivityId(),
attributes.GetActivityType().GetName(),
historyResponse.GetAttempt(),
historyResponse.GetClock(),
historyResponse.GetVersion(),
)
serializedToken, _ := e.tokenSerializer.Serialize(taskToken)
// This is here to ensure that this field is never nil as expected by the TS SDK.
// This may happen if ScheduleActivityExecution was recorded in version 1.23.
scheduleToCloseTimeout := attributes.ScheduleToCloseTimeout
if scheduleToCloseTimeout == nil {
scheduleToCloseTimeout = timestamp.DurationPtr(0)
}
return &matchingservice.PollActivityTaskQueueResponse{
ActivityId: attributes.ActivityId,
ActivityType: attributes.ActivityType,
Header: attributes.Header,
Input: attributes.Input,
WorkflowExecution: task.workflowExecution(),
CurrentAttemptScheduledTime: historyResponse.CurrentAttemptScheduledTime,
ScheduledTime: scheduledEvent.EventTime,
ScheduleToCloseTimeout: scheduleToCloseTimeout,
StartedTime: historyResponse.StartedTime,
StartToCloseTimeout: attributes.StartToCloseTimeout,
HeartbeatTimeout: attributes.HeartbeatTimeout,
TaskToken: serializedToken,
Attempt: taskToken.Attempt,
HeartbeatDetails: historyResponse.HeartbeatDetails,
WorkflowType: historyResponse.WorkflowType,
WorkflowNamespace: historyResponse.WorkflowNamespace,
PollerScalingDecision: task.pollerScalingDecision,
Priority: historyResponse.Priority,
RetryPolicy: historyResponse.RetryPolicy,
}
}
func (e *matchingEngineImpl) recordWorkflowTaskStarted(
ctx context.Context,
pollReq *workflowservice.PollWorkflowTaskQueueRequest,
task *internalTask,
) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
metrics.OperationCounter.With(e.metricsHandler).Record(
1,
metrics.OperationTag("RecordWorkflowTaskStarted"),
metrics.NamespaceTag(pollReq.Namespace),
metrics.TaskTypeTag(""), // Added to make tags consistent with history task executor.
)
if e.rateLimiter != nil {
err := e.rateLimiter.Wait(ctx, quotas.Request{
API: "RecordWorkflowTaskStarted",
Token: 1,
Caller: pollReq.Namespace,
CallerType: headers.CallerTypeAPI,
})
if err != nil {
return nil, err
}
}
ctx, cancel := newRecordTaskStartedContext(ctx, task)
defer cancel()
recordStartedRequest := &historyservice.RecordWorkflowTaskStartedRequest{
NamespaceId: task.event.Data.GetNamespaceId(),
WorkflowExecution: task.workflowExecution(),
ScheduledEventId: task.event.Data.GetScheduledEventId(),
Clock: task.event.Data.GetClock(),
RequestId: uuid.New(),
PollRequest: pollReq,
BuildIdRedirectInfo: task.redirectInfo,
// TODO: stop sending ScheduledDeployment. [cleanup-old-wv]
ScheduledDeployment: worker_versioning.DirectiveDeployment(task.event.Data.VersionDirective),
VersionDirective: task.event.Data.VersionDirective,
}
resp, err := e.historyClient.RecordWorkflowTaskStarted(ctx, recordStartedRequest)
if err != nil {
return nil, err
}
// History service can send history events in response.RawHistory. This happens when history.sendRawHistoryBetweenInternalServices is enabled.
// In that case use that directly. This is done to avoid deserializing history event blobs in history service.
// We need to process search attributes here since history service will not be able to do that on raw events.
if resp.RawHistory != nil {
resp.History = resp.RawHistory
err := api.ProcessOutgoingSearchAttributes(e.saProvider, e.saMapperProvider, resp.History.Events, task.namespace, e.visibilityManager)
if err != nil {
return nil, err
}
}
return resp, nil
}
func (e *matchingEngineImpl) recordActivityTaskStarted(
ctx context.Context,
pollReq *workflowservice.PollActivityTaskQueueRequest,
task *internalTask,
) (*historyservice.RecordActivityTaskStartedResponse, error) {
metrics.OperationCounter.With(e.metricsHandler).Record(
1,
metrics.OperationTag("RecordActivityTaskStarted"),
metrics.NamespaceTag(pollReq.Namespace),
metrics.TaskTypeTag(""), // Added to make tags consistent with history task executor.
)
if e.rateLimiter != nil {
err := e.rateLimiter.Wait(ctx, quotas.Request{
API: "RecordActivityTaskStarted",
Token: 1,
Caller: pollReq.Namespace,
CallerType: headers.CallerTypeAPI,
})
if err != nil {
return nil, err
}
}
ctx, cancel := newRecordTaskStartedContext(ctx, task)
defer cancel()
recordStartedRequest := &historyservice.RecordActivityTaskStartedRequest{
NamespaceId: task.event.Data.GetNamespaceId(),
WorkflowExecution: task.workflowExecution(),
ScheduledEventId: task.event.Data.GetScheduledEventId(),
Clock: task.event.Data.GetClock(),
RequestId: uuid.New(),
PollRequest: pollReq,
BuildIdRedirectInfo: task.redirectInfo,
Stamp: task.event.Data.GetStamp(),
// TODO: stop sending ScheduledDeployment. [cleanup-old-wv]
ScheduledDeployment: worker_versioning.DirectiveDeployment(task.event.Data.VersionDirective),
VersionDirective: task.event.Data.VersionDirective,
}
return e.historyClient.RecordActivityTaskStarted(ctx, recordStartedRequest)
}
// newRecordTaskStartedContext creates a context for recording
// activity or workflow task started. The parentCtx from
// pollActivity/WorkflowTaskQueue endpoint (which is a long poll
// API) has long timeout and unsuitable for recording task started,
// especially if the task is doing sync match and has caller
// (history transfer queue) waiting for response.
func newRecordTaskStartedContext(
parentCtx context.Context,
task *internalTask,
) (context.Context, context.CancelFunc) {
timeout := recordTaskStartedDefaultTimeout
if task.isSyncMatchTask() {
timeout = recordTaskStartedSyncMatchTimeout
}
return context.WithTimeout(parentCtx, timeout)
}
// Revives a deleted build ID updating its HLC timestamp.
// Returns a new build ID leaving the provided one untouched.
func (e *matchingEngineImpl) reviveBuildId(ns *namespace.Namespace, taskQueue string, buildId *persistencespb.BuildId) *persistencespb.BuildId {
// Bump the stamp and ensure it's newer than the deletion stamp.
prevStamp := common.CloneProto(buildId.StateUpdateTimestamp)
stamp := hlc.Next(prevStamp, e.timeSource)
stamp.ClusterId = e.clusterMeta.GetClusterID()
e.logger.Info("Revived build ID while applying replication event",
tag.WorkflowNamespace(ns.Name().String()),
tag.WorkflowTaskQueueName(taskQueue),
tag.BuildId(buildId.Id))
return &persistencespb.BuildId{
Id: buildId.GetId(),
State: persistencespb.STATE_ACTIVE,
StateUpdateTimestamp: stamp,
BecameDefaultTimestamp: buildId.BecameDefaultTimestamp,
}
}
// We use a very short timeout for considering a sticky worker available, since tasks can also
// be processed on the normal queue.
func stickyWorkerAvailable(pm taskQueuePartitionManager) bool {
return pm != nil && pm.HasPollerAfter("", time.Now().Add(-stickyPollerUnavailableWindow))
}
func buildRateLimitConfig(update *workflowservice.UpdateTaskQueueConfigRequest_RateLimitUpdate, updateTime *timestamppb.Timestamp, updateIdentity string) *taskqueuepb.RateLimitConfig {
var rateLimit *taskqueuepb.RateLimit
if r := update.GetRateLimit(); r != nil {
rateLimit = &taskqueuepb.RateLimit{RequestsPerSecond: r.RequestsPerSecond}
}
return &taskqueuepb.RateLimitConfig{
RateLimit: rateLimit,
Metadata: &taskqueuepb.ConfigMetadata{
Reason: update.GetReason(),
UpdateTime: updateTime,
UpdateIdentity: updateIdentity,
},
}
}
func prepareTaskQueueUserData(
tqud *persistencespb.TaskQueueUserData,
taskQueueType enumspb.TaskQueueType,
) *persistencespb.TaskQueueUserData {
data := common.CloneProto(tqud)
if data == nil {
data = &persistencespb.TaskQueueUserData{}
}
if data.PerType == nil {
data.PerType = make(map[int32]*persistencespb.TaskQueueTypeUserData)
}
tqType := int32(taskQueueType)
if data.PerType[tqType] == nil {
data.PerType[tqType] = &persistencespb.TaskQueueTypeUserData{}
}
if data.PerType[tqType].Config == nil {
data.PerType[tqType].Config = &taskqueuepb.TaskQueueConfig{}
}
return data
}
func (e *matchingEngineImpl) UpdateTaskQueueConfig(ctx context.Context, request *matchingservice.UpdateTaskQueueConfigRequest) (*matchingservice.UpdateTaskQueueConfigResponse, error) {
taskQueueFamily, err := tqid.NewTaskQueueFamily(request.NamespaceId, request.UpdateTaskqueueConfig.GetTaskQueue())
if err != nil {
return nil, err
}
taskQueueType := request.UpdateTaskqueueConfig.GetTaskQueueType()
// Get the partition manager for the root workflow partition of the task queue family.
// Configuration updates are applied here and eventually propagate,
// to all partitions and associated activity task queues of the same task queue family.
tqm, _, err := e.getTaskQueuePartitionManager(ctx,
taskQueueFamily.TaskQueue(enumspb.TASK_QUEUE_TYPE_WORKFLOW).RootPartition(),
true, loadCauseOtherWrite)
if err != nil {
return nil, err
}
if request.GetUpdateTaskqueueConfig() == nil {
tqud, _, err := tqm.GetUserDataManager().GetUserData()
if err != nil {
return nil, err
}
// If no update is requested, return the current config.
return &matchingservice.UpdateTaskQueueConfigResponse{
UpdatedTaskqueueConfig: tqud.GetData().GetPerType()[int32(taskQueueType)].GetConfig(),
}, nil
}
updateOptions := UserDataUpdateOptions{Source: "UpdateTaskQueueConfig"}
_, err = tqm.GetUserDataManager().UpdateUserData(ctx, updateOptions,
func(tqud *persistencespb.TaskQueueUserData) (*persistencespb.TaskQueueUserData, bool, error) {
data := prepareTaskQueueUserData(tqud, taskQueueType)
// Update timestamp from hlc clock
existingClock := data.Clock
if existingClock == nil {
existingClock = hlc.Zero(e.clusterMeta.GetClusterID())
}
now := hlc.Next(existingClock, e.timeSource)
protoTs := hlc.ProtoTimestamp(now)
// Update relevant config fields
cfg := data.PerType[int32(taskQueueType)].Config
updateTaskQueueConfig := request.GetUpdateTaskqueueConfig()
updateIdentity := updateTaskQueueConfig.GetIdentity()
// Queue Rate Limit
if qrl := updateTaskQueueConfig.GetUpdateQueueRateLimit(); qrl != nil {
cfg.QueueRateLimit = buildRateLimitConfig(qrl, protoTs, updateIdentity)
}
// Fairness Queue Rate Limit
if fkrl := updateTaskQueueConfig.GetUpdateFairnessKeyRateLimitDefault(); fkrl != nil {
cfg.FairnessKeysRateLimitDefault = buildRateLimitConfig(fkrl, protoTs, updateIdentity)
}
// Update the clock on TaskQueueUserData to enforce LWW on config updates
data.Clock = now
return data, true, nil
},
)
if err != nil {
return nil, err
}
userData, _, err := tqm.GetUserDataManager().GetUserData()
if err != nil {
return nil, err
}
return &matchingservice.UpdateTaskQueueConfigResponse{
UpdatedTaskqueueConfig: userData.GetData().GetPerType()[int32(taskQueueType)].GetConfig(),
}, nil
}