Files
tasks/service/matching/pri_task_reader.go
Alex ShtinandGitHub 8e8f1b0f7c Remove license header from every file (#7689)
## What changed?
<!-- Describe what has changed in this PR -->
Remove license header from every file. Because it is really hard to
follow in this PR here is the summary:
1. License header is removed from all `.go` and `.proto` files
:fireworks::fireworks:🎆.
2. `LICENSE` file in the root directory has only Temporal and Uber
copyrights.
3. 5 other `LICENSE` files added to the packages which have copyrights
different from Temporal and Uber: Datadog, Xargin, "Mat Ryer, Tyler
Bunnell and contributors".
4. `license_file` flag is removed from all code generation tools.
5. `copyright_file` flag is removed from `go:generate mockgen`
directive.
6. All copyright related targets are removed from `Makefile`.
7. Updated Temporal copyright year to 2025 everywhere.

## Why?
<!-- Tell your future self why have you made these changes -->
I double checked with legal department that it is not needed to have
license header in every file. One file per repo is enough. I put all
copyrights to the root `LICENSE` file and removed header from all other
files. Also updated tools and `Makefile`.
2025-05-01 18:50:21 -07:00

513 lines
16 KiB
Go

package matching
import (
"context"
"errors"
"slices"
"sync"
"time"
"github.com/emirpasic/gods/maps/treemap"
godsutils "github.com/emirpasic/gods/utils"
"go.temporal.io/api/serviceerror"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common"
"go.temporal.io/server/common/backoff"
"go.temporal.io/server/common/clock"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/persistence"
serviceerrors "go.temporal.io/server/common/serviceerror"
"go.temporal.io/server/common/softassert"
"go.temporal.io/server/common/util"
"golang.org/x/sync/semaphore"
)
const (
// TODO(pri): old matcher cleanup, move to here
// taskReaderThrottleRetryDelay = 3 * time.Second
concurrentAddRetries = 10
)
type (
priTaskReader struct {
backlogMgr *priBacklogManagerImpl
subqueue int
notifyC chan struct{} // Used as signal to notify pump of new tasks
logger log.Logger
lock sync.Mutex
backoffTimer *time.Timer
retrier backoff.Retrier
backlogAge backlogAgeTracker
addRetries *semaphore.Weighted
// ack manager state
outstandingTasks *treemap.Map // TaskID->acked
loadedTasks int
readLevel int64 // Maximum TaskID inserted into outstandingTasks
ackLevel int64 // Maximum TaskID below which all tasks are acked
// gc state
inGC bool
gcAckLevel int64 // last ack level GCed
lastGCTime time.Time // last time GCed
}
)
var addErrorRetryPolicy = backoff.NewExponentialRetryPolicy(2 * time.Second).
WithExpirationInterval(backoff.NoInterval)
func newPriTaskReader(
backlogMgr *priBacklogManagerImpl,
subqueue int,
initialAckLevel int64,
) *priTaskReader {
return &priTaskReader{
backlogMgr: backlogMgr,
subqueue: subqueue,
notifyC: make(chan struct{}, 1),
logger: backlogMgr.logger,
retrier: backoff.NewRetrier(
common.CreateReadTaskRetryPolicy(),
clock.NewRealTimeSource(),
),
backlogAge: newBacklogAgeTracker(),
addRetries: semaphore.NewWeighted(concurrentAddRetries),
// ack manager
outstandingTasks: treemap.NewWith(godsutils.Int64Comparator),
readLevel: initialAckLevel,
ackLevel: initialAckLevel,
}
}
// Start priTaskReader background goroutines.
func (tr *priTaskReader) Start() {
go tr.getTasksPump()
}
func (tr *priTaskReader) SignalTaskLoading() {
select {
case tr.notifyC <- struct{}{}:
default: // channel already has an event, don't block
}
}
func (tr *priTaskReader) getOldestBacklogTime() time.Time {
tr.lock.Lock()
defer tr.lock.Unlock()
return tr.backlogAge.oldestTime()
}
func (tr *priTaskReader) completeTask(task *internalTask, res taskResponse) {
err := res.startErr
if res.forwarded {
err = res.forwardErr
}
// We can handle some transient errors by just putting the task back in the matcher to
// match again. Note that for forwarded tasks, it's expected to get DeadlineExceeded when
// the task doesn't match on the root after backlogTaskForwardTimeout, and also expected to
// get errRemoteSyncMatchFailed, which is a serviceerror.Canceled error.
if err != nil && (common.IsServiceClientTransientError(err) ||
common.IsContextDeadlineExceededErr(err) ||
common.IsContextCanceledErr(err)) {
// TODO(pri): if this was a start error (not a forwarding error): consider adding a
// per-task backoff here, in case the error was workflow busy, we don't want to end up
// trying the same task immediately. maybe also: after a few attempts on the same task,
// let it get cycled to the end of the queue, in case there's some task/wf-specific
// thing.
tr.addTaskToMatcher(task)
return
}
// On other errors: ask backlog manager to re-spool to persistence
if err != nil {
if tr.backlogMgr.respoolTaskAfterError(task.event.Data) != nil {
return // task queue will unload now
}
}
tr.lock.Lock()
defer tr.lock.Unlock()
tr.backlogAge.record(task.event.AllocatedTaskInfo.Data.CreateTime, -1)
numAcked := tr.ackTaskLocked(task.event.TaskId)
tr.maybeGCLocked()
// use == so we just signal once when we cross this threshold
// TODO(pri): is this safe? maybe we need to improve this
if tr.loadedTasks == tr.backlogMgr.config.GetTasksReloadAt() {
tr.SignalTaskLoading()
}
tr.backlogMgr.db.updateAckLevelAndBacklogStats(tr.subqueue, tr.ackLevel, -numAcked, tr.backlogAge.oldestTime())
}
// nolint:revive // can simplify later
func (tr *priTaskReader) getTasksPump() {
ctx := tr.backlogMgr.tqCtx
tr.SignalTaskLoading() // prime pump
Loop:
for {
select {
case <-ctx.Done():
return
case <-tr.notifyC:
if tr.getLoadedTasks() > tr.backlogMgr.config.GetTasksReloadAt() {
// Too many loaded already, ignore this signal. We'll get another signal when
// loadedTasks drops low enough.
continue Loop
}
batch, err := tr.getTaskBatch(ctx)
tr.backlogMgr.signalIfFatal(err)
if err != nil {
// TODO: Should we ever stop retrying on db errors?
if common.IsResourceExhausted(err) {
tr.backoffSignal(taskReaderThrottleRetryDelay)
} else {
tr.backoffSignal(tr.retrier.NextBackOff(err))
}
continue Loop
}
tr.retrier.Reset()
if len(batch.tasks) == 0 {
tr.setReadLevelAfterGap(batch.readLevel)
if !batch.isReadBatchDone {
tr.SignalTaskLoading()
}
continue Loop
}
tr.processTaskBatch(batch.tasks)
// There may be more tasks.
tr.SignalTaskLoading()
}
}
}
// TODO(pri): old matcher cleanup: move here
// type getTasksBatchResponse struct {
// tasks []*persistencespb.AllocatedTaskInfo
// readLevel int64
// isReadBatchDone bool
// }
// Returns a batch of tasks from persistence starting form current read level.
// Also return a number that can be used to update readLevel
// Also return a bool to indicate whether read is finished
func (tr *priTaskReader) getTaskBatch(ctx context.Context) (getTasksBatchResponse, error) {
tr.lock.Lock()
readLevel := tr.readLevel
tr.lock.Unlock()
maxReadLevel := tr.backlogMgr.db.GetMaxReadLevel(tr.subqueue)
// counter i is used to break and let caller check whether taskqueue is still alive and needs to resume read.
for i := 0; i < 10 && readLevel < maxReadLevel; i++ {
upper := min(readLevel+tr.backlogMgr.config.RangeSize, maxReadLevel)
response, err := tr.backlogMgr.db.GetTasks(
ctx,
tr.subqueue,
readLevel+1,
upper+1,
tr.backlogMgr.config.GetTasksBatchSize(),
)
if err != nil {
return getTasksBatchResponse{}, err
}
// return as long as it grabs any tasks
if len(response.Tasks) > 0 {
return getTasksBatchResponse{tasks: response.Tasks}, nil
}
readLevel = upper
}
return getTasksBatchResponse{
tasks: nil,
readLevel: readLevel,
isReadBatchDone: readLevel == maxReadLevel,
}, nil // caller will update readLevel when no task grabbed
}
func (tr *priTaskReader) processTaskBatch(tasks []*persistencespb.AllocatedTaskInfo) {
tr.lock.Lock()
tasks = slices.DeleteFunc(tasks, func(t *persistencespb.AllocatedTaskInfo) bool {
tr.readLevel = max(tr.readLevel, t.TaskId)
if IsTaskExpired(t) {
metrics.ExpiredTasksPerTaskQueueCounter.With(tr.backlogMgr.metricsHandler).Record(1)
return true
}
// We may race to read tasks with signalNewTasks. If it wins, we may end up seeing
// tasks twice. In that case, we should just ignore them. If we win (based on
// readLevel), signalNewTasks will give up and signal us.
_, found := tr.outstandingTasks.Get(t.TaskId)
return found
})
tr.recordNewTasksLocked(tasks)
tr.lock.Unlock()
tr.addNewTasks(tasks)
}
// To add tasks to the matcher: call recordNewTasksLocked with tr.lock held, then release the
// lock and call addNewTasks. We call addTaskToMatcher outside tr.lock since it may take other
// locks to redirect the task.
func (tr *priTaskReader) recordNewTasksLocked(tasks []*persistencespb.AllocatedTaskInfo) {
// After we get to this point, we must eventually call task.finish or
// task.finishForwarded, which will call tr.completeTask.
for _, t := range tasks {
tr.outstandingTasks.Put(t.TaskId, false)
tr.loadedTasks++
tr.backlogAge.record(t.Data.CreateTime, 1)
}
}
// To add tasks to the matcher: call recordNewTasksLocked with tr.lock held, then release the
// lock and call addNewTasks. We call addTaskToMatcher outside tr.lock since it may take other
// locks to redirect the task.
func (tr *priTaskReader) addNewTasks(tasks []*persistencespb.AllocatedTaskInfo) {
for _, t := range tasks {
task := newInternalTaskFromBacklog(t, tr.completeTask)
tr.addTaskToMatcher(task)
}
}
func (tr *priTaskReader) addTaskToMatcher(task *internalTask) {
err := tr.backlogMgr.addSpooledTask(task)
if err == nil {
return
}
if drop, retry := tr.addErrorBehavior(err); drop {
task.finish(nil, false)
} else if retry {
// This should only be due to persistence problems. Retry in a new goroutine
// to not block other tasks, up to some concurrency limit.
if tr.addRetries.Acquire(tr.backlogMgr.tqCtx, 1) != nil {
return
}
go tr.retryAddAfterError(task)
}
}
func (tr *priTaskReader) addErrorBehavior(err error) (drop, retry bool) {
// addSpooledTask can only fail due to:
// - the task queue is closed (errTaskQueueClosed or context.Canceled)
// - ValidateDeployment failed (InvalidArgument)
// - versioning wants to get a versioned queue and it can't be initialized
// - versioning wants to re-spool the task on a different queue and that failed
// - versioning says StickyWorkerUnavailable
if errors.Is(err, errTaskQueueClosed) || common.IsContextCanceledErr(err) {
return false, false
}
var stickyUnavailable *serviceerrors.StickyWorkerUnavailable
if errors.As(err, &stickyUnavailable) {
return true, false // drop the task
}
var invalid *serviceerror.InvalidArgument
var internal *serviceerror.Internal
if errors.As(err, &invalid) || errors.As(err, &internal) {
tr.backlogMgr.throttledLogger.Error("nonretryable error processing spooled task", tag.Error(err))
return true, false // drop the task
}
// For any other error (this should be very rare), we can retry.
tr.backlogMgr.throttledLogger.Error("retryable error processing spooled task", tag.Error(err))
return false, true
}
func (tr *priTaskReader) retryAddAfterError(task *internalTask) {
defer tr.addRetries.Release(1)
metrics.BufferThrottlePerTaskQueueCounter.With(tr.backlogMgr.metricsHandler).Record(1)
// initial sleep since we just tried once
util.InterruptibleSleep(tr.backlogMgr.tqCtx, time.Second)
_ = backoff.ThrottleRetryContext(
tr.backlogMgr.tqCtx,
func(context.Context) error {
if IsTaskExpired(task.event.AllocatedTaskInfo) {
task.finish(nil, false)
return nil
}
err := tr.backlogMgr.addSpooledTask(task)
if drop, retry := tr.addErrorBehavior(err); drop {
task.finish(nil, false)
} else if retry {
metrics.BufferThrottlePerTaskQueueCounter.With(tr.backlogMgr.metricsHandler).Record(1)
return err
}
return nil
},
addErrorRetryPolicy,
nil,
)
}
func (tr *priTaskReader) signalNewTasks(resp subqueueCreateTasksResponse) {
tr.lock.Lock()
// We have to be very careful not to increment the read level past an ID that will somehow
// end up in the database, otherwise we might lose a task. We do this by verifying that our
// read level was equal to the previous max read level (i.e. we were at the end of the
// queue), and then we set it to the max read level as of CreateTasks.
// We also check that there's room in memory.
canAddDirect := tr.readLevel == resp.maxReadLevelBefore &&
(tr.loadedTasks+len(resp.tasks)) <= tr.backlogMgr.config.GetTasksBatchSize() &&
!slices.ContainsFunc(resp.tasks, func(t *persistencespb.AllocatedTaskInfo) bool {
// Because we checked readLevel, we know that getTasksPump can't have beat us to
// adding these tasks to outstandingTasks. So they should definitely not be there.
_, found := tr.outstandingTasks.Get(t.TaskId)
return softassert.That(tr.logger, !found, "newly-written task already present in outstanding tasks")
})
if !canAddDirect {
tr.lock.Unlock()
tr.SignalTaskLoading()
return
}
tr.readLevel = resp.maxReadLevelAfter
tr.recordNewTasksLocked(resp.tasks)
tr.lock.Unlock()
tr.addNewTasks(resp.tasks)
}
func (tr *priTaskReader) backoffSignal(duration time.Duration) {
tr.lock.Lock()
defer tr.lock.Unlock()
if tr.backoffTimer == nil {
tr.backoffTimer = time.AfterFunc(duration, func() {
tr.lock.Lock()
defer tr.lock.Unlock()
tr.SignalTaskLoading() // re-enqueue the event
tr.backoffTimer = nil
})
}
}
// ack manager
func (tr *priTaskReader) getLoadedTasks() int {
tr.lock.Lock()
defer tr.lock.Unlock()
return tr.loadedTasks
}
func (tr *priTaskReader) ackTaskLocked(taskId int64) int64 {
wasAlreadyAcked, found := tr.outstandingTasks.Get(taskId)
if !softassert.That(tr.logger, found, "completed task not found in oustandingTasks") {
return 0
}
if !softassert.That(tr.logger, !wasAlreadyAcked.(bool), "completed task was already acked") {
return 0
}
tr.outstandingTasks.Put(taskId, true)
tr.loadedTasks--
// Adjust the ack level as far as we can
var numAcked int64
for {
minId, acked := tr.outstandingTasks.Min()
if minId == nil || !acked.(bool) {
break
}
tr.ackLevel = minId.(int64) // nolint:revive
tr.outstandingTasks.Remove(minId)
numAcked += 1
}
return numAcked
}
func (tr *priTaskReader) setReadLevelAfterGap(newReadLevel int64) {
tr.lock.Lock()
defer tr.lock.Unlock()
if tr.ackLevel == tr.readLevel {
// This is called after we read a range and find no tasks. The range we read was tr.readLevel to newReadLevel.
// (We know this because nothing should change tr.readLevel except the getTasksPump loop itself, after initialization.
// And getTasksPump doesn't start until it gets a signal from taskWriter that it's initialized the levels.)
// If we've acked all tasks up to tr.readLevel, and there are no tasks between that and newReadLevel, then we've
// acked all tasks up to newReadLevel too. This lets us advance the ack level on a task queue with no activity
// but where the rangeid has moved higher, to prevent excessive reads on the next load.
tr.ackLevel = newReadLevel
}
tr.readLevel = newReadLevel
}
func (tr *priTaskReader) getLevels() (readLevel, ackLevel int64) {
tr.lock.Lock()
defer tr.lock.Unlock()
return tr.readLevel, tr.ackLevel
}
// gc
func (tr *priTaskReader) maybeGCLocked() {
if !tr.shouldGCLocked() {
return
}
tr.inGC = true
tr.lastGCTime = time.Now()
// gc in new goroutine so poller doesn't have to wait
go tr.doGC(tr.ackLevel)
}
func (tr *priTaskReader) shouldGCLocked() bool {
if tr.inGC {
return false
} else if gcGap := int(tr.ackLevel - tr.gcAckLevel); gcGap == 0 {
return false
} else if gcGap >= tr.backlogMgr.config.MaxTaskDeleteBatchSize() {
return true
}
return time.Since(tr.lastGCTime) > tr.backlogMgr.config.TaskDeleteInterval()
}
// called in new goroutine
func (tr *priTaskReader) doGC(ackLevel int64) {
batchSize := tr.backlogMgr.config.MaxTaskDeleteBatchSize()
ctx, cancel := context.WithTimeout(tr.backlogMgr.tqCtx, ioTimeout)
defer cancel()
n, err := tr.backlogMgr.db.CompleteTasksLessThan(ctx, ackLevel+1, batchSize, tr.subqueue)
tr.lock.Lock()
defer tr.lock.Unlock()
tr.inGC = false
if err != nil {
return
}
// implementation behavior for CompleteTasksLessThan:
// - unit test, cassandra: always return UnknownNumRowsAffected (in this case means "all")
// - sql: return number of rows affected (should be <= batchSize)
// if we get UnknownNumRowsAffected or a smaller number than our limit, we know we got
// everything <= ackLevel, so we can reset ours. if not, we may have to try again.
if n == persistence.UnknownNumRowsAffected || n < batchSize {
tr.gcAckLevel = ackLevel
}
}