go.temporal.io/server/service/matching/matcher.go
655 LOC · 378 covered · 277 uncovered · 130 ranges · 300 concepts · 93 introducers · 202 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
package matching
import (
"context"
"errors"
"math"
"sync"
"sync/atomic"
"time"
enumsspb "go.temporal.io/server/api/enums/v1"
"go.temporal.io/server/api/matchingservice/v1"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/primitives/timestamp"
"go.temporal.io/server/common/quotas"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// TaskMatcher matches a task producer with a task consumer
// Producers are usually rpc calls from history or taskReader
// that drains backlog from db. Consumers are the task queue pollers
type TaskMatcher struct {
config *taskQueueConfig
// synchronous task channel to match producer/consumer
taskC chan *internalTask
// synchronous task channel to match query task - the reason to have a
// separate channel for this is that there are cases where consumers
// are interested in queryTasks but not others. One example is when a
// namespace is not active in a cluster.
queryTaskC chan *internalTask
// channel closed when task queue is closed, to interrupt pollers
closeC chan struct{}
// rateLimiter that limits the rate at which tasks can be dispatched to consumers
rateLimiter quotas.RateLimiter
fwdr *Forwarder
metricsHandler metrics.Handler // namespace metric scope
backlogTasksCreateTime map[int64]int // task creation time (unix nanos) -> number of tasks with that time
backlogTasksLock sync.Mutex
lastPoller atomic.Int64 // unix nanos of most recent poll start time
waitingPollerCount atomic.Int64
}
var (
// Sentinel error to redirect while blocked in matcher.
errInterrupted = errors.New("interrupted offer")
errNoRecentPoller = status.Error(codes.FailedPrecondition, "no poller seen for task queue recently, worker may be down")
)
// newTaskMatcher returns a task matcher instance. The returned instance can be used by task producers and consumers to
// find a match. Both sync matches and non-sync matches should use this implementation
func newTaskMatcher(config *taskQueueConfig, fwdr *Forwarder, metricsHandler metrics.Handler, rateLimiter quotas.RateLimiter) *TaskMatcher {
matcher.go ×1
return &TaskMatcher{
config: config,
rateLimiter: rateLimiter,
metricsHandler: metricsHandler,
fwdr: fwdr,
taskC: make(chan *internalTask),
queryTaskC: make(chan *internalTask),
closeC: make(chan struct{}),
backlogTasksCreateTime: make(map[int64]int),
}
}
}
close(tm.closeC)
}
tm.rateLimiter.RecycleToken()
}
// Offer offers a task to a potential consumer (poller)
// If the task is successfully matched with a consumer, this
// method will return true and no error. If the task is matched
// but consumer returned error, then this method will return
// true and error message. This method should not be used for query
// task. This method should ONLY be used for sync match.
//
// When a local poller is not available and forwarding to a parent
// task queue partition is possible, this method will attempt forwarding
// to the parent partition.
//
// Cases when this method will block:
//
// Ratelimit:
// When a ratelimit token is not available, this method might block
// waiting for a token until the provided context timeout. Rate limits are
// not enforced for forwarded tasks from child partition.
//
// Forwarded tasks that originated from db backlog:
// When this method is called with a task that is forwarded from a
// remote partition and if (1) this task queue is root (2) task
// was from db backlog - this method will block until context timeout
// trying to match with a poller. The caller is expected to set the
// correct context timeout.
//
// returns error when:
// - ratelimit is exceeded (does not apply to query task)
// - context deadline is exceeded
// - task is matched and consumer returns error in response channel
func (tm *TaskMatcher) Offer(ctx context.Context, task *internalTask) (bool, error) {
matcher.go ×1
if !tm.isBacklogNegligible() {
// To ensure better dispatch ordering, we block sync match when a significant backlog is present.
matcher.go ×1
// Note that this check does not make a noticeable difference for history tasks, as they do not wait for a
// poller to become available. In presence of a backlog the chance of a poller being available when sync match
// request comes is almost zero.
// This check is mostly effective for the sync match requests that come from child partitions for spooled tasks.
return false, nil
}
return false, err
}
// because we waited on the rate limiter to offer this task,
// attach the rate limiter's RecycleToken func to the task
// so that if the task is later determined to be invalid,
// we can recycle the token it used.
}
if task.responseC != nil {
// if there is a response channel, block until resp is received
// and return error if the response contains error
err := <-task.responseC
if err.startErr == nil && !task.isForwarded() {
}
}
return false, nil
// no poller waiting for tasks, try forwarding this task to the
// root partition if possible
select {
if err := tm.fwdr.ForwardTask(ctx, task); err == nil {
token.release()
if !task.isForwarded() {
// if there are multiple forwarding hops, only the initial source partition emits this metric
tm.emitDispatchLatency(task, true)
}
}
if !tm.isForwardingAllowed() && // we are the root partition and forwarding is not possible
task.source == enumsspb.TASK_SOURCE_DB_BACKLOG && // task was from backlog (stored in db)
task.isForwarded() { // task came from a child partition
// to match with a poller until ctx timeout
return tm.offerOrTimeout(ctx, task)
}
}
}
}
func (tm *TaskMatcher) offerOrTimeout(ctx context.Context, task *internalTask) (bool, error) {
matcher.go ×3
select {
case tm.taskC <- task: // poller picked up the task
if task.responseC != nil {
select {
case err := <-task.responseC:
return true, err.startErr
case <-ctx.Done():
return false, nil
}
}
return false, nil
case <-ctx.Done():
return false, nil
}
}
func syncOfferTask[T any](
ctx context.Context,
tm *TaskMatcher,
task *internalTask,
taskChan chan *internalTask,
forwardFunc func(context.Context, *internalTask) (T, error),
returnNoPollerErr bool,
var t T
select {
<-task.responseC
return t, nil
}
var noPollerC <-chan time.Time
for {
if returnNoPollerErr {
if deadline, ok := ctx.Deadline(); ok && fwdrTokenC == nil {
// without having started the workers.
noPollerTimeout := time.Until(deadline) - returnEmptyTaskTimeBudget
t := time.NewTimer(noPollerTimeout)
noPollerC = t.C
defer t.Stop()
}
}
<-task.responseC
return t, nil
resp, err := forwardFunc(ctx, task)
token.release()
if err == nil {
}
// if we are rate limited, try only local match for the remainder of the context timeout
matcher.go ×2
// left
fwdrTokenC = nil
continue
}
// only error if there has not been a recent poller. Otherwise, let it wait for the remaining time
// hopping for a match, or ultimately returning the default CDE error.
if tm.timeSinceLastPoll() > tm.config.QueryPollerUnavailableWindow() {
}
return t, ctx.Err()
}
}
}
// OfferQuery will either match task to local poller or will forward query task.
// Local match is always attempted before forwarding is attempted. If local match occurs
// response and error are both nil, if forwarding occurs then response or error is returned.
func (tm *TaskMatcher) OfferQuery(ctx context.Context, task *internalTask) (*matchingservice.QueryWorkflowResponse, error) {
matcher.go ×1
return syncOfferTask(ctx, tm, task, tm.queryTaskC, tm.fwdr.ForwardQueryTask, true)
}
// OfferNexusTask either matchs a task to a local poller or forwards it if no local pollers available.
// Local match is always attempted before forwarding. If local match occurs response and error are both nil, if
// forwarding occurs then response or error is returned.
func (tm *TaskMatcher) OfferNexusTask(ctx context.Context, task *internalTask) (*matchingservice.DispatchNexusTaskResponse, error) {
matcher.go ×1
return syncOfferTask(ctx, tm, task, tm.taskC, tm.fwdr.ForwardNexusTask, false)
}
// MustOffer blocks until a consumer is found to handle this task
// Returns error only when context is canceled or the ratelimit is set to zero (allow nothing)
// The passed in context MUST NOT have a deadline associated with it
// Note that calling MustOffer is the only way that matcher knows there are spooled tasks in the
// backlog, in absence of a pending MustOffer call, the forwarding logic assumes that backlog is empty.
func (tm *TaskMatcher) MustOffer(ctx context.Context, task *internalTask, interruptCh <-chan struct{}) error {
matcher.go ×3
tm.registerBacklogTask(task)
defer tm.unregisterBacklogTask(task)
if err := tm.rateLimiter.Wait(ctx); err != nil {
}
// because we waited on the rate limiter to offer this task,
// attach the rate limiter's RecycleToken func to the task
// so that if the task is later determined to be invalid,
// we can recycle the token it used.
// attempt a match with local poller first. When that
// doesn't succeed, try both local match and remote match
select {
tm.emitDispatchLatency(task, false)
return nil
return ctx.Err()
}
defer func() {
}
}()
for {
fwdTokenC := tm.fwdrAddReqTokenC()
reconsiderFwdTimer = nil
var reconsiderFwdTimerC <-chan time.Time
if fwdTokenC != nil && !tm.isBacklogNegligible() {
// root and leaf partitions are treated equally and can process their
// backlog at the same rate. Stopping task forwarding, prevent poll
// forwarding as well (in presence of a backlog). This ensures all partitions
// receive polls and tasks at the same rate.
// Exception: we allow forward if this partition has not got any polls
// recently. This is helpful when there are very few pollers and they
// and they are all stuck in the wrong (root) partition. (Note that since
// frontend balanced the number of pending pollers per partition this only
// becomes an issue when the pollers are fewer than the partitions)
lp := tm.timeSinceLastPoll()
maxWaitForLocalPoller := tm.config.MaxWaitForPollerBeforeFwd()
if lp < maxWaitForLocalPoller {
fwdTokenC = nil
reconsiderFwdTimer = time.NewTimer(maxWaitForLocalPoller - lp)
reconsiderFwdTimerC = reconsiderFwdTimer.C
}
}
tm.emitDispatchLatency(task, false)
return nil
childCtx, cancel := context.WithTimeout(ctx, time.Second*2)
err := tm.fwdr.ForwardTask(childCtx, task)
token.release()
if err != nil {
// forwarder returns error only when the call is rate limited. To
// avoid a busy loop on such rate limiting events, we only attempt to make
// the next forwarded call after this childCtx expires. Till then, we block
// hoping for a local poller match
select {
case tm.taskC <- task:
cancel()
tm.emitDispatchLatency(task, false)
return nil
cancel()
return ctx.Err()
case <-interruptCh:
cancel()
return errInterrupted
}
continue forLoop
}
// at this point, we forwarded the task to a parent partition which
// in turn dispatched the task to a poller, because there was no error.
// Make sure we delete the task from the database.
task.finish(taskFinishResult{consumedToken: true})
tm.emitDispatchLatency(task, true)
return nil
return ctx.Err()
continue forLoop
case <-interruptCh:
return errInterrupted
}
}
}
func (tm *TaskMatcher) emitDispatchLatency(task *internalTask, forwarded bool) {
matcher.go ×1
if tm.config.EmitTaskDispatchLatencyAtPoll() {
return // metric will be emitted at poll response
}
if task.event.Data.CreateTime == nil {
return // should not happen but for safety
}
metrics.TaskDispatchLatencyPerTaskQueue.With(tm.metricsHandler).Record(
time.Since(timestamp.TimeValue(task.event.Data.CreateTime)),
metrics.StringTag("source", task.source.String()),
metrics.ForwardedTag(forwarded),
metrics.StringTag(metrics.TaskPriorityTagName, ""),
)
}
// Poll blocks until a task is found or context deadline is exceeded
// On success, the returned task could be a query task or a regular task
// Returns errNoTasks when context deadline is exceeded
func (tm *TaskMatcher) Poll(ctx context.Context, pollMetadata *pollMetadata) (*internalTask, error) {
matcher.go ×1
task, _, err := tm.poll(ctx, pollMetadata, false)
return task, err
}
// PollForQuery blocks until a *query* task is found or context deadline is exceeded
// Returns errNoTasks when context deadline is exceeded
func (tm *TaskMatcher) PollForQuery(ctx context.Context, pollMetadata *pollMetadata) (*internalTask, error) {
matcher.go ×2
task, _, err := tm.poll(ctx, pollMetadata, true)
return task, err
}
// unused in old matcher
}
func (tm *TaskMatcher) poll(
ctx context.Context, pollMetadata *pollMetadata, queryOnly bool,
taskC, queryTaskC := tm.taskC, tm.queryTaskC
if queryOnly {
}
tm.lastPoller.Store(start.UnixNano())
defer func() {
if pollMetadata.forwardedFrom == "" {
// Only recording for original polls
var pollResult string
if err == nil {
} else {
pollResult = "failed"
}
time.Since(start),
metrics.ForwardedTag(forwardedPoll),
metrics.StringTag(metrics.TaskPriorityTagName, ""),
metrics.PollResultTag(pollResult),
)
}
tm.emitForwardedSourceStats(task.isForwarded(), pollMetadata.forwardedFrom, forwardedPoll)
matcher.go ×3
}
}()
// We want to effectively do a prioritized select, but Go select is random
// if multiple cases are ready, so split into multiple selects.
// The priority order is:
// 1. ctx.Done or tm.closeC
// 2. taskC and queryTaskC
// 3. forwarding
// 4. block looking locally for remainder of context lifetime
// To correctly handle priorities and allow any case to succeed, all select
// statements except for the last one must be non-blocking, and the last one
// must include all the previous cases.
// 1. ctx.Done
metrics.PollTimeoutPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
return nil, false, errNoTasks
return nil, false, errNoTasks
}
// 2. taskC and queryTaskC
if task.responseC != nil {
metrics.PollSuccessWithSyncPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
matcher.go ×3
}
return task, false, nil
case task := <-queryTaskC:
metrics.PollSuccessWithSyncPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
metrics.PollSuccessPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
return task, false, nil
}
// From here on the goroutine will block on taskC (in step 3 or 4), so it
// is ready to receive a sync-matched task.
defer tm.waitingPollerCount.Add(-1)
if tm.isBacklogNegligible() {
// We don't forward pollers if there is a non-negligible backlog in this partition.
select {
metrics.PollTimeoutPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
return nil, false, errNoTasks
return nil, false, errNoTasks
if task.responseC != nil {
metrics.PollSuccessWithSyncPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
matcher.go ×1
}
return task, false, nil
metrics.PollSuccessWithSyncPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
metrics.PollSuccessPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
return task, false, nil
// Arrange to cancel this request if closeC is closed
fwdCtx, cancel := contextWithCancelOnChannelClose(ctx, tm.closeC)
task, err := tm.fwdr.ForwardPoll(fwdCtx, pollMetadata)
cancel()
token.release()
if err == nil {
}
}
}
// 4. blocking local poll
metrics.PollTimeoutPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
return nil, false, errNoTasks
case <-tm.closeC:
return nil, false, errNoTasks
if task.responseC != nil {
metrics.PollSuccessWithSyncPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
matcher.go ×2
}
return task, false, nil
case task := <-queryTaskC:
metrics.PollSuccessWithSyncPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
metrics.PollSuccessPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
return task, false, nil
}
}
if tm.fwdr == nil {
}
}
if tm.fwdr == nil {
}
}
return tm.fwdr != nil
}
// isBacklogNegligible returns true of the age of backlog is less than the threshold. Note that this relies on
// MustOffer being called when there is a backlog, otherwise we'd not know.
return tm.getBacklogAge() < tm.config.BacklogNegligibleAge()
}
if task.event.Data.CreateTime == nil {
}
defer tm.backlogTasksLock.Unlock()
ts := timestamp.TimeValue(task.event.Data.CreateTime).UnixNano()
tm.backlogTasksCreateTime[ts] += 1
}
if task.event.Data.CreateTime == nil {
}
defer tm.backlogTasksLock.Unlock()
ts := timestamp.TimeValue(task.event.Data.CreateTime).UnixNano()
counter := tm.backlogTasksCreateTime[ts]
if counter == 1 {
delete(tm.backlogTasksCreateTime, ts)
} else {
}
}
// getBacklogAge is the latest age across all backlogs re-directing to this matcher; may momentarily
// be 0 cause of race conditions when no reader pushes a task into the matcher at this moment
tm.backlogTasksLock.Lock()
defer tm.backlogTasksLock.Unlock()
if len(tm.backlogTasksCreateTime) == 0 {
}
for createTime := range tm.backlogTasksCreateTime {
oldest = min(oldest, createTime)
}
}
func (tm *TaskMatcher) emitForwardedSourceStats(
isTaskForwarded bool,
pollForwardedSource string,
forwardedPoll bool,
if forwardedPoll {
// This means we forwarded the poll to another partition. Skipping this to prevent duplicate emits.
matcher.go ×2
// Only the partition in which the match happened should emit this metric.
return
}
switch {
case isTaskForwarded && isPollForwarded:
metrics.RemoteToRemoteMatchPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
metrics.RemoteToLocalMatchPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
case isPollForwarded:
metrics.LocalToRemoteMatchPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
metrics.LocalToLocalMatchPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
}
}
return time.Since(time.Unix(0, tm.lastPoller.Load()))
}
// HasWaitingPoller returns true if it there's a poller ready and waiting
// this is mostly useful in testing to avoid test races on setup
return tm.waitingPollerCount.Load() > 0
}
// contextWithCancelOnChannelClose returns a child Context and CancelFunc just like
// context.WithCancel, but additionally propagates cancellation from another channel (besides
// the parent's cancellation channel).
func contextWithCancelOnChannelClose(parent context.Context, closeC <-chan struct{}) (context.Context, context.CancelFunc) {
matcher.go ×3
ctx, cancel := context.WithCancel(parent)
go func() {
select {
cancel()
}
}()
}