mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? Include all poll metadata (including rate limit request) in forwarded activity polls. ## Why? This has no real behavior change (the rate limit should be the same on all partitions already), but it fixes the rate limit reported in poller info for the root partition, which was getting cleared on forwarded polls and creating confusion.
701 lines
24 KiB
Go
701 lines
24 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 (
|
|
"context"
|
|
"errors"
|
|
"math"
|
|
"strconv"
|
|
"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{}
|
|
|
|
// dynamicRate is the dynamic rate & burst for rate limiter
|
|
dynamicRateBurst quotas.MutableRateBurst
|
|
// dynamicRateLimiter is the dynamic rate limiter that can be used to force refresh on new rates.
|
|
dynamicRateLimiter *quotas.DynamicRateLimiterImpl
|
|
// forceRefreshRateOnce is used to force refresh rate limit for first time
|
|
forceRefreshRateOnce sync.Once
|
|
// rateLimiter that limits the rate at which tasks can be dispatched to consumers
|
|
rateLimiter quotas.RateLimiter
|
|
|
|
fwdr *Forwarder
|
|
metricsHandler metrics.Handler // namespace metric scope
|
|
numPartitions func() int // number of task queue partitions
|
|
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
|
|
}
|
|
|
|
const (
|
|
defaultTaskDispatchRPS = 100000.0
|
|
defaultTaskDispatchRPSTTL = time.Minute
|
|
emptyBacklogAge time.Duration = -1
|
|
)
|
|
|
|
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) *TaskMatcher {
|
|
dynamicRateBurst := quotas.NewMutableRateBurst(
|
|
defaultTaskDispatchRPS,
|
|
int(defaultTaskDispatchRPS),
|
|
)
|
|
dynamicRateLimiter := quotas.NewDynamicRateLimiter(
|
|
dynamicRateBurst,
|
|
defaultTaskDispatchRPSTTL,
|
|
)
|
|
limiter := quotas.NewMultiRateLimiter([]quotas.RateLimiter{
|
|
dynamicRateLimiter,
|
|
quotas.NewDefaultOutgoingRateLimiter(
|
|
config.AdminNamespaceTaskQueueToPartitionDispatchRate,
|
|
),
|
|
quotas.NewDefaultOutgoingRateLimiter(
|
|
config.AdminNamespaceToPartitionDispatchRate,
|
|
),
|
|
})
|
|
return &TaskMatcher{
|
|
config: config,
|
|
dynamicRateBurst: dynamicRateBurst,
|
|
dynamicRateLimiter: dynamicRateLimiter,
|
|
rateLimiter: limiter,
|
|
metricsHandler: metricsHandler,
|
|
fwdr: fwdr,
|
|
taskC: make(chan *internalTask),
|
|
queryTaskC: make(chan *internalTask),
|
|
closeC: make(chan struct{}),
|
|
numPartitions: config.NumReadPartitions,
|
|
backlogTasksCreateTime: make(map[int64]int),
|
|
}
|
|
}
|
|
|
|
func (tm *TaskMatcher) Stop() {
|
|
close(tm.closeC)
|
|
}
|
|
|
|
// 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) {
|
|
if !tm.isBacklogNegligible() {
|
|
// To ensure better dispatch ordering, we block sync match when a significant backlog is present.
|
|
// 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
|
|
}
|
|
|
|
if !task.isForwarded() {
|
|
if err := tm.rateLimiter.Wait(ctx); err != nil {
|
|
metrics.SyncThrottlePerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
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.
|
|
task.recycleToken = tm.rateLimiter.RecycleToken
|
|
}
|
|
|
|
select {
|
|
case tm.taskC <- task: // poller picked up the task
|
|
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 == nil && !task.isForwarded() {
|
|
tm.emitDispatchLatency(task, false)
|
|
}
|
|
return true, err
|
|
}
|
|
return false, nil
|
|
default:
|
|
// no poller waiting for tasks, try forwarding this task to the
|
|
// root partition if possible
|
|
select {
|
|
case token := <-tm.fwdrAddReqTokenC():
|
|
if err := tm.fwdr.ForwardTask(ctx, task); err == nil {
|
|
// task was remotely sync matched on the parent partition
|
|
token.release()
|
|
tm.emitDispatchLatency(task, true)
|
|
return true, nil
|
|
}
|
|
token.release()
|
|
default:
|
|
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
|
|
// a forwarded backlog task from a child partition, block trying
|
|
// to match with a poller until ctx timeout
|
|
return tm.offerOrTimeout(ctx, task)
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func (tm *TaskMatcher) offerOrTimeout(ctx context.Context, task *internalTask) (bool, error) {
|
|
select {
|
|
case tm.taskC <- task: // poller picked up the task
|
|
if task.responseC != nil {
|
|
select {
|
|
case err := <-task.responseC:
|
|
return true, err
|
|
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,
|
|
) (T, error) {
|
|
var t T
|
|
select {
|
|
case taskChan <- task:
|
|
<-task.responseC
|
|
return t, nil
|
|
default:
|
|
}
|
|
|
|
fwdrTokenC := tm.fwdrAddReqTokenC()
|
|
var noPollerC <-chan time.Time
|
|
|
|
for {
|
|
if returnNoPollerErr {
|
|
returnNoPollerErr = false // only do this once
|
|
if deadline, ok := ctx.Deadline(); ok && fwdrTokenC == nil {
|
|
// Reserving 1sec to customize the timeout error if user is querying a workflow
|
|
// without having started the workers.
|
|
noPollerTimeout := time.Until(deadline) - returnEmptyTaskTimeBudget
|
|
t := time.NewTimer(noPollerTimeout)
|
|
noPollerC = t.C
|
|
defer t.Stop()
|
|
}
|
|
}
|
|
|
|
select {
|
|
case taskChan <- task:
|
|
<-task.responseC
|
|
return t, nil
|
|
case token := <-fwdrTokenC:
|
|
resp, err := forwardFunc(ctx, task)
|
|
token.release()
|
|
if err == nil {
|
|
return resp, nil
|
|
}
|
|
if errors.Is(err, errForwarderSlowDown) {
|
|
// if we are rate limited, try only local match for the remainder of the context timeout
|
|
// left
|
|
fwdrTokenC = nil
|
|
continue
|
|
}
|
|
return t, err
|
|
case <-noPollerC:
|
|
// 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, errNoRecentPoller
|
|
}
|
|
continue
|
|
case <-ctx.Done():
|
|
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) {
|
|
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) {
|
|
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 {
|
|
tm.registerBacklogTask(task)
|
|
defer tm.unregisterBacklogTask(task)
|
|
|
|
if err := tm.rateLimiter.Wait(ctx); err != nil {
|
|
return 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.
|
|
task.recycleToken = tm.rateLimiter.RecycleToken
|
|
|
|
// attempt a match with local poller first. When that
|
|
// doesn't succeed, try both local match and remote match
|
|
select {
|
|
case tm.taskC <- task:
|
|
tm.emitDispatchLatency(task, false)
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
var reconsiderFwdTimer *time.Timer
|
|
defer func() {
|
|
if reconsiderFwdTimer != nil {
|
|
reconsiderFwdTimer.Stop()
|
|
}
|
|
}()
|
|
|
|
forLoop:
|
|
for {
|
|
fwdTokenC := tm.fwdrAddReqTokenC()
|
|
reconsiderFwdTimer = nil
|
|
var reconsiderFwdTimerC <-chan time.Time
|
|
if fwdTokenC != nil && !tm.isBacklogNegligible() {
|
|
// If there is a non-negligible backlog, we stop forwarding to make sure
|
|
// 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
|
|
}
|
|
}
|
|
|
|
select {
|
|
case tm.taskC <- task:
|
|
tm.emitDispatchLatency(task, false)
|
|
return nil
|
|
case token := <-fwdTokenC:
|
|
childCtx, cancel := context.WithTimeout(ctx, time.Second*2)
|
|
err := tm.fwdr.ForwardTask(childCtx, task)
|
|
token.release()
|
|
if err != nil {
|
|
metrics.ForwardTaskErrorsPerTaskQueue.With(tm.metricsHandler).Record(1)
|
|
// 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
|
|
case <-childCtx.Done():
|
|
case <-ctx.Done():
|
|
cancel()
|
|
return ctx.Err()
|
|
case <-interruptCh:
|
|
cancel()
|
|
return errInterrupted
|
|
}
|
|
cancel()
|
|
continue forLoop
|
|
}
|
|
cancel()
|
|
// 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(nil, true)
|
|
tm.emitDispatchLatency(task, true)
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-reconsiderFwdTimerC:
|
|
continue forLoop
|
|
case <-interruptCh:
|
|
return errInterrupted
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tm *TaskMatcher) emitDispatchLatency(task *internalTask, forwarded bool) {
|
|
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.StringTag("forwarded", strconv.FormatBool(forwarded)),
|
|
)
|
|
}
|
|
|
|
// 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) {
|
|
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) {
|
|
task, _, err := tm.poll(ctx, pollMetadata, true)
|
|
return task, err
|
|
}
|
|
|
|
// UpdateRatelimit updates the task dispatch rate
|
|
func (tm *TaskMatcher) UpdateRatelimit(rps float64) {
|
|
nPartitions := float64(tm.numPartitions())
|
|
if nPartitions > 0 {
|
|
// divide the rate equally across all partitions
|
|
rps = rps / nPartitions
|
|
}
|
|
burst := int(math.Ceil(rps))
|
|
|
|
minTaskThrottlingBurstSize := tm.config.MinTaskThrottlingBurstSize()
|
|
if burst < minTaskThrottlingBurstSize {
|
|
burst = minTaskThrottlingBurstSize
|
|
}
|
|
|
|
tm.dynamicRateBurst.SetRPS(rps)
|
|
tm.dynamicRateBurst.SetBurst(burst)
|
|
tm.forceRefreshRateOnce.Do(func() {
|
|
// Dynamic rate limiter only refresh its rate every 1m. Before that initial 1m interval, it uses default rate
|
|
// which is 10K and is too large in most cases. We need to force refresh for the first time this rate is set
|
|
// by poller. Only need to do that once. If the rate change later, it will be refresh in 1m.
|
|
tm.dynamicRateLimiter.Refresh()
|
|
})
|
|
}
|
|
|
|
// Rate returns the current rate at which tasks are dispatched
|
|
func (tm *TaskMatcher) Rate() float64 {
|
|
return tm.rateLimiter.Rate()
|
|
}
|
|
|
|
func (tm *TaskMatcher) poll(
|
|
ctx context.Context, pollMetadata *pollMetadata, queryOnly bool,
|
|
) (task *internalTask, forwardedPoll bool, err error) {
|
|
taskC, queryTaskC := tm.taskC, tm.queryTaskC
|
|
if queryOnly {
|
|
taskC = nil
|
|
}
|
|
|
|
start := time.Now()
|
|
tm.lastPoller.Store(start.UnixNano())
|
|
|
|
defer func() {
|
|
if pollMetadata.forwardedFrom == "" {
|
|
// Only recording for original polls
|
|
metrics.PollLatencyPerTaskQueue.With(tm.metricsHandler).Record(
|
|
time.Since(start), metrics.StringTag("forwarded", strconv.FormatBool(forwardedPoll)))
|
|
}
|
|
|
|
if err == nil {
|
|
tm.emitForwardedSourceStats(task.isForwarded(), pollMetadata.forwardedFrom, forwardedPoll)
|
|
}
|
|
}()
|
|
|
|
// 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
|
|
select {
|
|
case <-ctx.Done():
|
|
metrics.PollTimeoutPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
return nil, false, errNoTasks
|
|
case <-tm.closeC:
|
|
return nil, false, errNoTasks
|
|
default:
|
|
}
|
|
|
|
// 2. taskC and queryTaskC
|
|
select {
|
|
case task := <-taskC:
|
|
if task.responseC != nil {
|
|
metrics.PollSuccessWithSyncPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
}
|
|
metrics.PollSuccessPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
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
|
|
default:
|
|
}
|
|
|
|
if tm.isBacklogNegligible() {
|
|
// 3. forwarding (and all other clauses repeated)
|
|
// We don't forward pollers if there is a non-negligible backlog in this partition.
|
|
select {
|
|
case <-ctx.Done():
|
|
metrics.PollTimeoutPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
return nil, false, errNoTasks
|
|
case <-tm.closeC:
|
|
return nil, false, errNoTasks
|
|
case task := <-taskC:
|
|
if task.responseC != nil {
|
|
metrics.PollSuccessWithSyncPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
}
|
|
metrics.PollSuccessPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
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
|
|
case token := <-tm.fwdrPollReqTokenC():
|
|
// 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 {
|
|
return task, true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. blocking local poll
|
|
select {
|
|
case <-ctx.Done():
|
|
metrics.PollTimeoutPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
return nil, false, errNoTasks
|
|
case <-tm.closeC:
|
|
return nil, false, errNoTasks
|
|
case task := <-taskC:
|
|
if task.responseC != nil {
|
|
metrics.PollSuccessWithSyncPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
}
|
|
metrics.PollSuccessPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
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
|
|
}
|
|
}
|
|
|
|
func (tm *TaskMatcher) fwdrPollReqTokenC() <-chan *ForwarderReqToken {
|
|
if tm.fwdr == nil {
|
|
return nil
|
|
}
|
|
return tm.fwdr.PollReqTokenC()
|
|
}
|
|
|
|
func (tm *TaskMatcher) fwdrAddReqTokenC() <-chan *ForwarderReqToken {
|
|
if tm.fwdr == nil {
|
|
return nil
|
|
}
|
|
return tm.fwdr.AddReqTokenC()
|
|
}
|
|
|
|
func (tm *TaskMatcher) isForwardingAllowed() bool {
|
|
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.
|
|
func (tm *TaskMatcher) isBacklogNegligible() bool {
|
|
return tm.getBacklogAge() < tm.config.BacklogNegligibleAge()
|
|
}
|
|
|
|
func (tm *TaskMatcher) registerBacklogTask(task *internalTask) {
|
|
if task.event.Data.CreateTime == nil {
|
|
return // should not happen but for safety
|
|
}
|
|
|
|
tm.backlogTasksLock.Lock()
|
|
defer tm.backlogTasksLock.Unlock()
|
|
|
|
ts := timestamp.TimeValue(task.event.Data.CreateTime).UnixNano()
|
|
tm.backlogTasksCreateTime[ts] += 1
|
|
}
|
|
|
|
func (tm *TaskMatcher) unregisterBacklogTask(task *internalTask) {
|
|
if task.event.Data.CreateTime == nil {
|
|
return // should not happen but for safety
|
|
}
|
|
|
|
tm.backlogTasksLock.Lock()
|
|
defer tm.backlogTasksLock.Unlock()
|
|
|
|
ts := timestamp.TimeValue(task.event.Data.CreateTime).UnixNano()
|
|
counter := tm.backlogTasksCreateTime[ts]
|
|
if counter == 1 {
|
|
delete(tm.backlogTasksCreateTime, ts)
|
|
} else {
|
|
tm.backlogTasksCreateTime[ts] = counter - 1
|
|
}
|
|
}
|
|
|
|
// 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
|
|
func (tm *TaskMatcher) getBacklogAge() time.Duration {
|
|
tm.backlogTasksLock.Lock()
|
|
defer tm.backlogTasksLock.Unlock()
|
|
|
|
if len(tm.backlogTasksCreateTime) == 0 {
|
|
return emptyBacklogAge
|
|
}
|
|
|
|
oldest := int64(math.MaxInt64)
|
|
for createTime := range tm.backlogTasksCreateTime {
|
|
oldest = min(oldest, createTime)
|
|
}
|
|
|
|
return time.Since(time.Unix(0, oldest))
|
|
}
|
|
|
|
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.
|
|
// Only the partition in which the match happened should emit this metric.
|
|
return
|
|
}
|
|
|
|
isPollForwarded := len(pollForwardedSource) > 0
|
|
switch {
|
|
case isTaskForwarded && isPollForwarded:
|
|
metrics.RemoteToRemoteMatchPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
case isTaskForwarded:
|
|
metrics.RemoteToLocalMatchPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
case isPollForwarded:
|
|
metrics.LocalToRemoteMatchPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
default:
|
|
metrics.LocalToLocalMatchPerTaskQueueCounter.With(tm.metricsHandler).Record(1)
|
|
}
|
|
}
|
|
|
|
func (tm *TaskMatcher) timeSinceLastPoll() time.Duration {
|
|
return time.Since(time.Unix(0, tm.lastPoller.Load()))
|
|
}
|
|
|
|
// 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) {
|
|
ctx, cancel := context.WithCancel(parent)
|
|
go func() {
|
|
select {
|
|
case <-closeC:
|
|
cancel()
|
|
case <-ctx.Done():
|
|
}
|
|
}()
|
|
return ctx, cancel
|
|
}
|