mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? Change to send raw history blobs from history service to frontend service. History service returns a new proto message that has a repeated bytes history field. This response is wire compatible with the original response which has temporal.api.history.v1.History type for this field. This allows history service to not deserialize events from this data blob. This considerably reduces CPU usage. History service still needs event_id and version decoded from history events. For this we use a new proto message StrippedHistoryEvent which has these two fields only. It takes considerably less CPU to decode events to this struct. ## Why? We have seen incidents of high history CPU usage when large number of GetWorkflowExecutionHistory calls are made to workflows which has large history. With this change we can reduce the CPU burden on history service during this API call. ## How did you test it? Existing unit and functional tests. ## Potential risks ## Documentation ## Is hotfix candidate?
2690 lines
105 KiB
Go
2690 lines
105 KiB
Go
// The MIT License
|
|
//
|
|
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
|
|
//
|
|
// Copyright (c) 2020 Uber Technologies, Inc.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
package matching
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
"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/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/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/durationpb"
|
|
"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 (
|
|
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
|
|
}
|
|
|
|
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
|
|
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
|
|
}
|
|
)
|
|
|
|
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,
|
|
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,
|
|
) Engine {
|
|
scopedMetricsHandler := metricsHandler.WithTags(metrics.OperationTag(metrics.MatchingEngineScope))
|
|
e := &matchingEngineImpl{
|
|
status: common.DaemonStatusInitialized,
|
|
taskManager: taskManager,
|
|
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]](),
|
|
}
|
|
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
|
|
}
|
|
|
|
func (e *matchingEngineImpl) String() string {
|
|
// Executes taskQueue.String() on each task queue outside of lock
|
|
buf := new(bytes.Buffer)
|
|
for _, l := range e.getTaskQueuePartitions(1000) {
|
|
fmt.Fprintf(buf, "\n%s", l.String())
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// 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) {
|
|
defer func() {
|
|
if retErr != nil || retPM == nil {
|
|
return
|
|
}
|
|
|
|
if retErr = retPM.WaitUntilInitialized(ctx); retErr != nil {
|
|
e.unloadTaskQueuePartition(retPM, unloadCauseInitError)
|
|
}
|
|
}()
|
|
|
|
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)
|
|
var newPM *taskQueuePartitionManagerImpl
|
|
onFatalErr := func(cause unloadCause) { newPM.unloadFromEngine(cause) }
|
|
userDataManager := newUserDataManager(e.taskManager, e.matchingRawClient, onFatalErr, 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()
|
|
if newPM.Partition().IsRoot() {
|
|
// Whenever a root partition is loaded we need to force all other 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.
|
|
|
|
newPM.ForceLoadAllNonRootPartitions()
|
|
}
|
|
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,
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
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()
|
|
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 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:
|
|
if e.config.MatchingDropNonRetryableTasks() {
|
|
e.nonRetryableErrorsDropTask(task, taskQueueName, err)
|
|
// drop the task as otherwise task would be stuck in a retry-loop
|
|
task.finish(nil, false)
|
|
} else {
|
|
// default case
|
|
task.finish(err, 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.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.DeploymentFromCapabilities(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.WorkflowEventID(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()
|
|
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:
|
|
if e.config.MatchingDropNonRetryableTasks() {
|
|
e.nonRetryableErrorsDropTask(task, taskQueueName, err)
|
|
// drop the task as otherwise task would be stuck in a retry-loop
|
|
task.finish(nil, false)
|
|
} else {
|
|
// default case
|
|
task.finish(err, 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.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.DeploymentFromCapabilities(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.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.DeploymentFromCapabilities(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()
|
|
if req.ApiMode == enumspb.DESCRIBE_TASK_QUEUE_MODE_ENHANCED {
|
|
rootPartition, err := tqid.PartitionFromProto(req.GetTaskQueue(), request.GetNamespaceId(), req.GetTaskQueueType())
|
|
tqConfig := newTaskQueueConfig(rootPartition.TaskQueue(), e.config, namespace.Name(req.Namespace))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
|
|
timeSinceLastFanOut := rootPM.TimeSinceLastFanOut()
|
|
lastFanOutTTL := tqConfig.TaskQueueInfoByBuildIdTTL()
|
|
|
|
// TODO bug fix: We cache the same map regardless of VersionSelection or TaskQueueTypes, so if someone queries the Activity Task Queue type of this task queue name, we cache that result and return it even if the next DescribeTQ call is about the WF TQ
|
|
physicalInfoByBuildId := make(map[string]map[enumspb.TaskQueueType]*taskqueuespb.PhysicalTaskQueueInfo)
|
|
if timeSinceLastFanOut > lastFanOutTTL {
|
|
// collect internal info
|
|
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 {
|
|
if _, ok := physicalInfoByBuildId[buildId]; !ok {
|
|
physicalInfoByBuildId[buildId] = make(map[enumspb.TaskQueueType]*taskqueuespb.PhysicalTaskQueueInfo)
|
|
}
|
|
if physInfo, ok := physicalInfoByBuildId[buildId][taskQueueType]; !ok {
|
|
physicalInfoByBuildId[buildId][taskQueueType] = vii.PhysicalTaskQueueInfo
|
|
} else {
|
|
var mergedStats *taskqueuepb.TaskQueueStats
|
|
|
|
// only report Task Queue Statistics if requested.
|
|
if req.GetReportStats() {
|
|
totalStats := physicalInfoByBuildId[buildId][taskQueueType].TaskQueueStats
|
|
partitionStats := vii.PhysicalTaskQueueInfo.TaskQueueStats
|
|
|
|
mergedStats = &taskqueuepb.TaskQueueStats{
|
|
ApproximateBacklogCount: totalStats.ApproximateBacklogCount + partitionStats.ApproximateBacklogCount,
|
|
ApproximateBacklogAge: largerBacklogAge(totalStats.ApproximateBacklogAge, partitionStats.ApproximateBacklogAge),
|
|
TasksAddRate: totalStats.TasksAddRate + partitionStats.TasksAddRate,
|
|
TasksDispatchRate: totalStats.TasksDispatchRate + partitionStats.TasksDispatchRate,
|
|
}
|
|
}
|
|
merged := &taskqueuespb.PhysicalTaskQueueInfo{
|
|
Pollers: dedupPollers(append(physInfo.GetPollers(), vii.PhysicalTaskQueueInfo.GetPollers()...)),
|
|
TaskQueueStats: mergedStats,
|
|
}
|
|
physicalInfoByBuildId[buildId][taskQueueType] = merged
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// update cache
|
|
rootPM.UpdateTimeSinceLastFanOutAndCache(physicalInfoByBuildId)
|
|
} else {
|
|
// fetch info from rootPartition's cache
|
|
physicalInfoByBuildId = rootPM.GetPhysicalTaskQueueInfoFromCache()
|
|
}
|
|
|
|
// smush internal info into versions info
|
|
versionsInfo := make(map[string]*taskqueuepb.TaskQueueVersionInfo, 0)
|
|
for bid, typeMap := range physicalInfoByBuildId {
|
|
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
|
|
}
|
|
// Otherwise, do legacy DescribeTaskQueue
|
|
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
|
|
}
|
|
return pm.LegacyDescribeTaskQueue(req.GetIncludeTaskQueueStatus())
|
|
}
|
|
|
|
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.NewFailedPrecondition(
|
|
fmt.Sprintf("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.NewInvalidArgument(fmt.Sprintf("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 := 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 := 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 := 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()) {
|
|
mergedData.AssignmentRules = currentVersioningData.GetAssignmentRules()
|
|
mergedData.RedirectRules = currentVersioningData.GetRedirectRules()
|
|
mergedUserData.PerType = current.GetPerType()
|
|
} else {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// No need to keep the 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,
|
|
},
|
|
}, 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.NewFailedPrecondition(fmt.Sprintf("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.NewFailedPrecondition("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 = newChildContext(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.NewFailedPrecondition("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.NewBackgroundCallerInfo(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
|
|
}
|
|
|
|
// 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 := newChildContext(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()
|
|
}
|
|
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,
|
|
}
|
|
}
|
|
|
|
func (e *matchingEngineImpl) recordWorkflowTaskStarted(
|
|
ctx context.Context,
|
|
pollReq *workflowservice.PollWorkflowTaskQueueRequest,
|
|
task *internalTask,
|
|
) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
|
|
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,
|
|
}
|
|
|
|
return e.historyClient.RecordWorkflowTaskStarted(ctx, recordStartedRequest)
|
|
}
|
|
|
|
func (e *matchingEngineImpl) recordActivityTaskStarted(
|
|
ctx context.Context,
|
|
pollReq *workflowservice.PollActivityTaskQueueRequest,
|
|
task *internalTask,
|
|
) (*historyservice.RecordActivityTaskStartedResponse, error) {
|
|
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))
|
|
}
|
|
|
|
// largerBacklogAge returns the larger BacklogAge
|
|
func largerBacklogAge(rootBacklogAge *durationpb.Duration, currentPartitionAge *durationpb.Duration) *durationpb.Duration {
|
|
if rootBacklogAge.AsDuration() > currentPartitionAge.AsDuration() {
|
|
return rootBacklogAge
|
|
}
|
|
return currentPartitionAge
|
|
}
|