mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? Allow draining backlog from the other table when moving from tasks v1 to v2 and back (i.e. classic to fairness-enabled). Exiting draining mode when draining is complete is not implemented yet. ## Why? To allow migrating to fairness without losing tasks. ## How did you test it? - [x] added new functional test(s) ## Potential risks We need to get the rangeid lock on both tables when migrating, which slows down initialization. This is only the first time and while migration is active, after the old tasks are drained then we won't need to lock the old table. That part isn't implemented yet, so this is off by default.
235 lines
6.4 KiB
Go
235 lines
6.4 KiB
Go
package matching
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
enumspb "go.temporal.io/api/enums/v1"
|
|
"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/log"
|
|
"go.temporal.io/server/common/log/tag"
|
|
"go.temporal.io/server/common/metrics"
|
|
"go.temporal.io/server/common/persistence"
|
|
"go.temporal.io/server/common/softassert"
|
|
)
|
|
|
|
type (
|
|
writeTaskRequest struct {
|
|
taskInfo *persistencespb.TaskInfo
|
|
responseCh chan<- error
|
|
subqueue subqueueIndex // for priTaskWriter only
|
|
fairLevel // filled in by taskWriterLoop
|
|
}
|
|
|
|
taskIDBlock struct {
|
|
start int64
|
|
end int64
|
|
}
|
|
|
|
// taskWriter writes tasks sequentially to persistence
|
|
taskWriter struct {
|
|
backlogMgr *backlogManagerImpl
|
|
config *taskQueueConfig
|
|
db *taskQueueDB
|
|
logger log.Logger
|
|
appendCh chan *writeTaskRequest
|
|
taskIDBlock taskIDBlock
|
|
currentTaskIDBlock taskIDBlock // copy of taskIDBlock for safe concurrent access via getCurrentTaskIDBlock()
|
|
}
|
|
)
|
|
|
|
var (
|
|
// errShutdown indicates that the task queue is shutting down
|
|
errShutdown = &persistence.ConditionFailedError{Msg: "task queue shutting down"}
|
|
errNonContiguousBlocks = errors.New("previous block end is not equal to current block")
|
|
|
|
noTaskIDs = taskIDBlock{start: 1, end: 0}
|
|
)
|
|
|
|
func newTaskWriter(
|
|
backlogMgr *backlogManagerImpl,
|
|
) *taskWriter {
|
|
return &taskWriter{
|
|
backlogMgr: backlogMgr,
|
|
config: backlogMgr.config,
|
|
db: backlogMgr.db,
|
|
logger: backlogMgr.logger,
|
|
appendCh: make(chan *writeTaskRequest, backlogMgr.config.OutstandingTaskAppendsThreshold()),
|
|
taskIDBlock: noTaskIDs,
|
|
}
|
|
}
|
|
|
|
// Start taskWriter background goroutine.
|
|
func (w *taskWriter) Start() {
|
|
go w.taskWriterLoop()
|
|
}
|
|
|
|
func (w *taskWriter) initReadWriteState() error {
|
|
state, err := w.renewLeaseWithRetry(foreverRetryPolicy, common.IsPersistenceTransientError)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
softassert.That(w.logger, !state.otherHasTasks, "found otherHasTasks in classic metadata")
|
|
w.taskIDBlock = rangeIDToTaskIDBlock(state.rangeID, w.config.RangeSize)
|
|
w.currentTaskIDBlock = w.taskIDBlock
|
|
w.backlogMgr.taskAckManager.setAckLevel(state.ackLevel)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *taskWriter) appendTask(
|
|
taskInfo *persistencespb.TaskInfo,
|
|
) error {
|
|
|
|
select {
|
|
case <-w.backlogMgr.tqCtx.Done():
|
|
return errShutdown
|
|
default:
|
|
// noop
|
|
}
|
|
|
|
startTime := time.Now().UTC()
|
|
ch := make(chan error)
|
|
req := &writeTaskRequest{
|
|
taskInfo: taskInfo,
|
|
responseCh: ch,
|
|
}
|
|
|
|
select {
|
|
case w.appendCh <- req:
|
|
select {
|
|
case err := <-ch:
|
|
metrics.TaskWriteLatencyPerTaskQueue.With(w.backlogMgr.metricsHandler).Record(time.Since(startTime))
|
|
return err
|
|
case <-w.backlogMgr.tqCtx.Done():
|
|
// if we are shutting down, this request will never make
|
|
// it to persistence, just bail out and fail this request
|
|
return errShutdown
|
|
}
|
|
default: // channel is full, throttle
|
|
metrics.TaskWriteThrottlePerTaskQueueCounter.With(w.backlogMgr.metricsHandler).Record(1)
|
|
return &serviceerror.ResourceExhausted{
|
|
Cause: enumspb.RESOURCE_EXHAUSTED_CAUSE_SYSTEM_OVERLOADED,
|
|
Scope: enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE,
|
|
Message: "Too many outstanding appends to the task queue",
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *taskWriter) assignTaskIDs(reqs []*writeTaskRequest) error {
|
|
for i := range reqs {
|
|
if w.taskIDBlock.start > w.taskIDBlock.end {
|
|
// we ran out of current allocation block
|
|
newBlock, err := w.allocTaskIDBlock(w.taskIDBlock.end)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.taskIDBlock = newBlock
|
|
}
|
|
reqs[i].id = w.taskIDBlock.start
|
|
w.taskIDBlock.start++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *taskWriter) appendTasks(reqs []*writeTaskRequest) error {
|
|
_, err := w.db.CreateTasks(w.backlogMgr.tqCtx, reqs)
|
|
if err != nil {
|
|
w.backlogMgr.signalIfFatal(err)
|
|
w.logger.Error("Persistent store operation failure",
|
|
tag.StoreOperationCreateTask,
|
|
tag.Error(err),
|
|
tag.WorkflowTaskQueueName(w.backlogMgr.queueKey().PersistenceName()),
|
|
tag.WorkflowTaskQueueType(w.backlogMgr.queueKey().TaskType()))
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *taskWriter) taskWriterLoop() {
|
|
err := w.initReadWriteState()
|
|
w.backlogMgr.SetInitializedError(err)
|
|
|
|
for {
|
|
atomic.StoreInt64(&w.currentTaskIDBlock.start, w.taskIDBlock.start)
|
|
atomic.StoreInt64(&w.currentTaskIDBlock.end, w.taskIDBlock.end)
|
|
|
|
select {
|
|
case request := <-w.appendCh:
|
|
// read a batch of requests from the channel
|
|
reqs := []*writeTaskRequest{request}
|
|
reqs = w.getWriteBatch(reqs)
|
|
|
|
err := w.assignTaskIDs(reqs)
|
|
if err == nil {
|
|
err = w.appendTasks(reqs)
|
|
}
|
|
for _, req := range reqs {
|
|
req.responseCh <- err
|
|
}
|
|
|
|
case <-w.backlogMgr.tqCtx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *taskWriter) getWriteBatch(reqs []*writeTaskRequest) []*writeTaskRequest {
|
|
readLoop:
|
|
for i := 0; i < w.config.MaxTaskBatchSize(); i++ {
|
|
select {
|
|
case req := <-w.appendCh:
|
|
reqs = append(reqs, req)
|
|
default: // channel is empty, don't block
|
|
break readLoop
|
|
}
|
|
}
|
|
return reqs
|
|
}
|
|
|
|
func (w *taskWriter) renewLeaseWithRetry(
|
|
retryPolicy backoff.RetryPolicy,
|
|
retryErrors backoff.IsRetryable,
|
|
) (taskQueueState, error) {
|
|
var newState taskQueueState
|
|
op := func(ctx context.Context) (err error) {
|
|
newState, err = w.db.RenewLease(ctx)
|
|
return
|
|
}
|
|
metrics.LeaseRequestPerTaskQueueCounter.With(w.backlogMgr.metricsHandler).Record(1)
|
|
err := backoff.ThrottleRetryContext(w.backlogMgr.tqCtx, op, retryPolicy, retryErrors)
|
|
if err != nil {
|
|
metrics.LeaseFailurePerTaskQueueCounter.With(w.backlogMgr.metricsHandler).Record(1)
|
|
return newState, err
|
|
}
|
|
return newState, nil
|
|
}
|
|
|
|
func (w *taskWriter) allocTaskIDBlock(prevBlockEnd int64) (taskIDBlock, error) {
|
|
currBlock := rangeIDToTaskIDBlock(w.db.RangeID(), w.config.RangeSize)
|
|
if currBlock.end != prevBlockEnd {
|
|
return taskIDBlock{}, errNonContiguousBlocks
|
|
}
|
|
state, err := w.renewLeaseWithRetry(persistenceOperationRetryPolicy, common.IsPersistenceTransientError)
|
|
if err != nil {
|
|
if w.backlogMgr.signalIfFatal(err) {
|
|
return taskIDBlock{}, errShutdown
|
|
}
|
|
return taskIDBlock{}, err
|
|
}
|
|
return rangeIDToTaskIDBlock(state.rangeID, w.config.RangeSize), nil
|
|
}
|
|
|
|
// getCurrentTaskIDBlock returns the current taskIDBlock. Safe to be called concurrently.
|
|
func (w *taskWriter) getCurrentTaskIDBlock() taskIDBlock {
|
|
return taskIDBlock{
|
|
start: atomic.LoadInt64(&w.currentTaskIDBlock.start),
|
|
end: atomic.LoadInt64(&w.currentTaskIDBlock.end),
|
|
}
|
|
}
|